Klimi's new dotfiles with stow.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

864 lines
33 KiB

4 years ago
  1. ;;; biblio-core.el --- A framework for looking up and displaying bibliographic entries -*- lexical-binding: t -*-
  2. ;; Copyright (C) 2016 Clément Pit-Claudel
  3. ;; Author: Clément Pit-Claudel <clement.pitclaudel@live.com>
  4. ;; Version: 0.2
  5. ;; Package-Version: 20190624.1408
  6. ;; Package-Requires: ((emacs "24.3") (let-alist "1.0.4") (seq "1.11") (dash "2.12.1"))
  7. ;; Keywords: bib, tex, convenience, hypermedia
  8. ;; URL: http://github.com/cpitclaudel/biblio.el
  9. ;; This program is free software; you can redistribute it and/or modify
  10. ;; it under the terms of the GNU General Public License as published by
  11. ;; the Free Software Foundation, either version 3 of the License, or
  12. ;; (at your option) any later version.
  13. ;;
  14. ;; This program is distributed in the hope that it will be useful,
  15. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. ;; GNU General Public License for more details.
  18. ;;
  19. ;; You should have received a copy of the GNU General Public License
  20. ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. ;;; Commentary:
  22. ;; A framework for browsing bibliographic search results. This is the core
  23. ;; package; for user interfaces, see any of `biblio-crossref', `biblio-dblp', `biblio-doi',
  24. ;; `biblio-arxiv', `biblio-hal' and `biblio-dissemin', which are part of the `biblio' package.
  25. ;;; Code:
  26. (require 'bibtex)
  27. (require 'browse-url)
  28. (require 'hl-line)
  29. (require 'ido)
  30. (require 'json)
  31. (require 'url-queue)
  32. (require 'dash)
  33. (require 'let-alist)
  34. (require 'seq)
  35. (defvar-local biblio--target-buffer nil
  36. "Buffer into which BibTeX entries should be inserted.
  37. This variable is local to each search results buffer.")
  38. (defvar-local biblio--search-terms nil
  39. "Keywords that led to a page of bibliographic search results.")
  40. (defvar-local biblio--backend nil
  41. "Backend that produced a page of bibliographic search results.")
  42. (defgroup biblio nil
  43. "A browser for bibliographic information."
  44. :group 'communication)
  45. (defgroup biblio-core nil
  46. "Core of the biblio package."
  47. :group 'biblio)
  48. (defgroup biblio-faces nil
  49. "Faces of the biblio package."
  50. :group 'biblio)
  51. (defcustom biblio-synchronous nil
  52. "Whether bibliographic queries should be synchronous."
  53. :group 'biblio-core
  54. :type 'boolean)
  55. (defcustom biblio-authors-limit 10
  56. "Maximum number of authors to display per paper."
  57. :group 'biblio-core
  58. :type 'integer)
  59. ;;; Compatibility
  60. (defun biblio-alist-get (key alist)
  61. "Copy of Emacs 25's `alist-get', minus default.
  62. Get the value associated to KEY in ALIST, or nil."
  63. (cdr (assq key alist)))
  64. (defun biblio--plist-to-alist (plist)
  65. "Copy of Emacs 25's `json--plist-to-alist'.
  66. Return an alist of the property-value pairs in PLIST."
  67. (let (res)
  68. (while plist
  69. (let ((prop (pop plist))
  70. (val (pop plist)))
  71. (push (cons prop val) res)))
  72. (nreverse res)))
  73. ;;; Utilities
  74. (defconst biblio--bibtex-entry-format
  75. (list 'opts-or-alts 'numerical-fields 'page-dashes 'whitespace
  76. 'inherit-booktitle 'realign 'last-comma 'delimiters
  77. 'unify-case 'braces 'strings 'sort-fields)
  78. "Format to use in `biblio-format-bibtex'.
  79. See `bibtex-entry-format' for details; this list is all
  80. transformations, except errors for missing fields.
  81. Also see `biblio-cleanup-bibtex-function'.")
  82. (defun biblio--cleanup-bibtex-1 (dialect autokey)
  83. "Cleanup BibTeX entry starting at point.
  84. DIALECT is `BibTeX' or `biblatex'. AUTOKEY: see `biblio-format-bibtex'."
  85. (let ((bibtex-entry-format biblio--bibtex-entry-format)
  86. (bibtex-align-at-equal-sign t)
  87. (bibtex-autokey-edit-before-use nil)
  88. (bibtex-autokey-year-title-separator ":"))
  89. ;; Use biblatex to allow for e.g. @Online
  90. ;; Use BibTeX to allow for e.g. @TechReport
  91. (bibtex-set-dialect dialect t)
  92. (bibtex-clean-entry autokey)))
  93. (defun biblio--cleanup-bibtex (autokey)
  94. "Default value of `biblio-cleanup-bibtex-function'.
  95. AUTOKEY: See biblio-format-bibtex."
  96. (save-excursion
  97. (when (search-forward "@data{" nil t)
  98. (replace-match "@misc{")))
  99. (ignore-errors ;; See https://github.com/crosscite/citeproc-doi-server/issues/12
  100. (condition-case _
  101. (biblio--cleanup-bibtex-1 'biblatex autokey)
  102. (error (biblio--cleanup-bibtex-1 'BibTeX autokey)))))
  103. (defcustom biblio-cleanup-bibtex-function
  104. #'biblio--cleanup-bibtex
  105. "Function to clean up BibTeX entries.
  106. This function is called in a `bibtex-mode' buffer containing an
  107. unprocessed, potentially invalid BibTeX (or BibLaTeX) entry, and
  108. should clean it up in place. It should take a single argument,
  109. AUTOKEY, indicating whether the entry needs a new key."
  110. :group 'biblio
  111. :type 'function)
  112. (defun biblio-format-bibtex (bibtex &optional autokey)
  113. "Format BIBTEX entry.
  114. With non-nil AUTOKEY, automatically generate a key for BIBTEX."
  115. (with-temp-buffer
  116. (bibtex-mode)
  117. (save-excursion
  118. (insert (biblio-strip bibtex)))
  119. (when (functionp biblio-cleanup-bibtex-function)
  120. (funcall biblio-cleanup-bibtex-function autokey))
  121. (if (fboundp 'font-lock-ensure) (font-lock-ensure)
  122. (with-no-warnings (font-lock-fontify-buffer)))
  123. (buffer-substring-no-properties (point-min) (point-max))))
  124. (defun biblio--beginning-of-response-body ()
  125. "Move point to beginning of response body."
  126. (goto-char (point-min))
  127. (unless (re-search-forward "^\n" nil t)
  128. (error "Invalid response from server: %S" (buffer-string))))
  129. (defun biblio-response-as-utf-8 ()
  130. "Extract body of response."
  131. (set-buffer-multibyte t)
  132. (decode-coding-region (point) (point-max) 'utf-8 t))
  133. (defun biblio-decode-url-buffer (coding)
  134. "Decode URL buffer with CODING."
  135. (set-buffer-multibyte t) ;; URL buffer is unibyte
  136. (decode-coding-region (point-min) (point-max) coding))
  137. (defun biblio--event-error-code (event)
  138. "Extract HTTP error code from EVENT, if any."
  139. (pcase event
  140. (`(:error . (error ,source ,details))
  141. (cons source details))))
  142. (eval-and-compile
  143. (define-error 'biblio--url-error "URL retrieval error."))
  144. (defun biblio--throw-on-unexpected-errors (errors allowed-errors)
  145. "Throw an url-error for any error in ERRORS not in ALLOWED-ERRORS."
  146. (dolist (err errors)
  147. (cond ((eq (car err) 'url-queue-timeout)
  148. (signal 'biblio--url-error 'timeout))
  149. ((not (member err allowed-errors))
  150. (signal 'biblio--url-error err)))))
  151. (defun biblio--extract-errors (events)
  152. "Extract errors from EVENTS."
  153. (delq nil (mapcar #'biblio--event-error-code (biblio--plist-to-alist events))))
  154. (defun biblio-generic-url-callback (callback &optional cleanup-function &rest allowed-errors)
  155. "Make an `url'-ready callback from CALLBACK.
  156. CALLBACK is called with no arguments; the buffer containing the
  157. server's response is current at the time of the call, and killed
  158. after the call returns. Call CLEANUP-FUNCTION before checking
  159. for errors. If the request returns one of the errors in
  160. ALLOWED-ERRORS, CALLBACK is instead called with one argument, the
  161. list of allowed errors that occurred instead of a buffer. If the
  162. request returns another error, an exception is raised."
  163. (lambda (events)
  164. (let ((target-buffer (current-buffer)))
  165. (unwind-protect
  166. (progn
  167. (funcall (or cleanup-function #'ignore))
  168. (condition-case err
  169. (-if-let* ((errors (biblio--extract-errors events)))
  170. (progn
  171. (biblio--throw-on-unexpected-errors errors allowed-errors)
  172. (funcall callback errors))
  173. (biblio--beginning-of-response-body)
  174. (delete-region (point-min) (point))
  175. (funcall callback))
  176. (error (message "Error while processing request: %S" err))))
  177. (kill-buffer target-buffer)))))
  178. (defun biblio-url-retrieve (url callback)
  179. "Wrapper around `url-queue-retrieve'.
  180. URL and CALLBACK; see `url-queue-retrieve'"
  181. (message "Fetching %s" url)
  182. (if biblio-synchronous
  183. (with-current-buffer (url-retrieve-synchronously url)
  184. (funcall callback nil))
  185. (setq url-queue-timeout 1)
  186. (url-queue-retrieve url callback)))
  187. (defun biblio-strip (str)
  188. "Remove spaces surrounding STR."
  189. (when str
  190. (->> str
  191. (replace-regexp-in-string "[ \t\n\r]+\\'" "")
  192. (replace-regexp-in-string "\\`[ \t\n\r]+" ""))))
  193. (defun biblio-cleanup-doi (doi)
  194. "Cleanup DOI string."
  195. (biblio-strip (replace-regexp-in-string "https?://\\(dx\\.\\)?doi\\.org/" "" doi)))
  196. (defun biblio-remove-empty (strs)
  197. "Remove empty sequences from STRS."
  198. (seq-remove #'seq-empty-p strs))
  199. (defun biblio-join-1 (sep strs)
  200. "Join non-empty elements of STRS with SEP."
  201. (declare (indent 1))
  202. (let ((strs (biblio-remove-empty strs)))
  203. (mapconcat #'identity strs sep)))
  204. (defun biblio-join (sep &rest strs)
  205. "Join non-empty elements of STRS with SEP."
  206. (declare (indent 1))
  207. (biblio-join-1 sep strs))
  208. (defmacro biblio--with-text-property (prop value &rest body)
  209. "Set PROP to VALUE on text inserted by BODY."
  210. (declare (indent 2)
  211. (debug t))
  212. (let ((beg-var (make-symbol "beg")))
  213. `(let ((,beg-var (point)))
  214. ,@body
  215. (put-text-property ,beg-var (point) ,prop ,value))))
  216. (defmacro biblio-with-fontification (face &rest body)
  217. "Apply FACE to text inserted by BODY."
  218. (declare (indent 1)
  219. (debug t))
  220. (let ((beg-var (make-symbol "beg")))
  221. `(let ((,beg-var (point)))
  222. ,@body
  223. (font-lock-append-text-property ,beg-var (point) 'face ,face))))
  224. ;;; Help with major mode
  225. (defsubst biblio--as-list (x)
  226. "Make X a list, if it isn't."
  227. (if (consp x) x (list x)))
  228. (defun biblio--map-keymap (func map)
  229. "Call `map-keymap' on FUNC and MAP, and collect the results."
  230. (let ((out))
  231. (map-keymap (lambda (&rest args) (push (apply func args) out)) map)
  232. out))
  233. (defun biblio--flatten-map (keymap &optional prefix)
  234. "Flatten KEYMAP, prefixing its keys with PREFIX.
  235. This should really be in Emacs core (in Elisp), instead of being
  236. implemented in C (at least for sparse keymaps). Don't run this on
  237. non-sparse keymaps."
  238. (nreverse
  239. (cond
  240. ((keymapp keymap)
  241. (seq-map (lambda (key-value)
  242. "Add PREFIX to key in KEY-VALUE."
  243. (cons (append prefix (biblio--as-list (car key-value)))
  244. (cdr key-value)))
  245. (delq nil
  246. (apply
  247. #'seq-concatenate
  248. 'list (biblio--map-keymap
  249. (lambda (k v)
  250. "Return a list of bindings in V, prefixed by K."
  251. (biblio--flatten-map v (biblio--as-list k)))
  252. keymap)))))
  253. ;; This breaks if keymap is a symbol whose function cell is a keymap
  254. ((symbolp keymap)
  255. (list (cons prefix keymap))))))
  256. (defun biblio--group-alist (alist)
  257. "Return a copy of ALIST whose keys are lists of keys, grouped by value.
  258. That is, if two key map to `eq' values, they are grouped."
  259. (let ((map (make-hash-table :test 'eq))
  260. (new-alist nil))
  261. (pcase-dolist (`(,key . ,value) alist)
  262. (puthash value (cons key (gethash value map)) map))
  263. (pcase-dolist (`(,_ . ,value) alist)
  264. (-when-let* ((keys (gethash value map)))
  265. (push (cons (nreverse keys) value) new-alist)
  266. (puthash value nil map)))
  267. (nreverse new-alist)))
  268. (defun biblio--quote (str)
  269. "Quote STR and call `substitute-command-keys' on it."
  270. (if str (substitute-command-keys (concat "`" str "'")) ""))
  271. (defun biblio--quote-keys (keys)
  272. "Quote and concatenate keybindings in KEYS."
  273. (mapconcat (lambda (keyseq)
  274. (biblio--quote (ignore-errors (help-key-description keyseq nil))))
  275. keys ", "))
  276. (defun biblio--brief-docs (command)
  277. "Return first line of documentation of COMMAND."
  278. (let ((docs (or (ignore-errors (documentation command t)) "")))
  279. (string-match "\\(.*\\)$" docs)
  280. (match-string-no-properties 1 docs)))
  281. (defun biblio--help-with-major-mode-1 (keyseqs-command)
  282. "Print help on KEYSEQS-COMMAND to standard output."
  283. ;; (biblio-with-fontification 'font-lock-function-name-face
  284. (insert (format "%s (%S)\n"
  285. (biblio--quote-keys (car keyseqs-command))
  286. (cdr keyseqs-command)))
  287. (biblio-with-fontification 'font-lock-doc-face
  288. (insert (format " %s\n\n" (biblio--brief-docs (cdr keyseqs-command))))))
  289. (defun biblio--help-with-major-mode ()
  290. "Display help with current major mode."
  291. (let ((buf (format "*%S help*" major-mode)))
  292. (with-help-window buf
  293. (princ (format "Help with %s\n\n" (biblio--quote (symbol-name major-mode))))
  294. (let ((bindings (nreverse
  295. (biblio--group-alist
  296. (biblio--flatten-map
  297. (current-local-map))))))
  298. (with-current-buffer buf
  299. (seq-do #'biblio--help-with-major-mode-1 bindings))))
  300. buf))
  301. ;;; Interaction
  302. (defconst biblio--search-result-marker-regexp "^> "
  303. "Indicator of a search result.")
  304. (defun biblio--selection-move (move-fn search-fn)
  305. "Move using MOVE-FN, then call SEARCH-FN and go to first match."
  306. (let ((target (point)))
  307. (save-excursion
  308. (funcall move-fn)
  309. (when (funcall search-fn biblio--search-result-marker-regexp nil t)
  310. (setq target (match-end 0))))
  311. (goto-char target)))
  312. (defun biblio-get-url (metadata)
  313. "Compute a url from METADATA.
  314. Uses .url, and .doi as a fallback."
  315. (let-alist metadata
  316. (if .url .url
  317. (when .doi
  318. (concat "https://doi.org/" (url-encode-url .doi))))))
  319. (defun biblio--selection-browse ()
  320. "Open the web page of the current entry in a web browser."
  321. (interactive)
  322. (-if-let* ((url (biblio-get-url (biblio--selection-metadata-at-point))))
  323. (browse-url url)
  324. (user-error "This record does not contain a URL")))
  325. (defun biblio--selection-browse-direct ()
  326. "Open the full text of the current entry in a web browser."
  327. (interactive)
  328. (-if-let* ((url (biblio-alist-get 'direct-url (biblio--selection-metadata-at-point))))
  329. (browse-url url)
  330. (user-error "This record does not contain a direct URL (try arXiv or HAL)")))
  331. (defun biblio--selection-next ()
  332. "Move to next search result."
  333. (interactive)
  334. (biblio--selection-move #'end-of-line #'re-search-forward))
  335. (defun biblio--selection-first ()
  336. "Move to first search result."
  337. (goto-char (point-min))
  338. (biblio--selection-move #'ignore #'re-search-forward))
  339. (defun biblio--selection-previous ()
  340. "Move to previous search result."
  341. (interactive)
  342. (biblio--selection-move #'beginning-of-line #'re-search-backward))
  343. (defun biblio--selection-copy-callback (bibtex entry)
  344. "Add BIBTEX (from ENTRY) to kill ring."
  345. (kill-new bibtex)
  346. (message "Killed bibtex entry for %S."
  347. (biblio--prepare-title (biblio-alist-get 'title entry))))
  348. (defun biblio--selection-copy ()
  349. "Copy BibTeX of current entry at point."
  350. (interactive)
  351. (biblio--selection-forward-bibtex #'biblio--selection-copy-callback))
  352. (defun biblio--selection-copy-quit ()
  353. "Copy BibTeX of current entry at point and close results."
  354. (interactive)
  355. (biblio--selection-forward-bibtex #'biblio--selection-copy-callback t))
  356. (defun biblio--target-window ()
  357. "Get the window of the source buffer."
  358. (get-buffer-window biblio--target-buffer))
  359. (defun biblio--selection-insert-callback (bibtex entry)
  360. "Add BIBTEX (from ENTRY) to kill ring."
  361. (let ((target-buffer biblio--target-buffer))
  362. (with-selected-window (or (biblio--target-window) (selected-window))
  363. (with-current-buffer target-buffer
  364. (insert bibtex "\n\n"))))
  365. (message "Inserted bibtex entry for %S."
  366. (biblio--prepare-title (biblio-alist-get 'title entry))))
  367. (defun biblio--selection-insert ()
  368. "Insert BibTeX of current entry into source buffer."
  369. (interactive)
  370. (biblio--selection-forward-bibtex #'biblio--selection-insert-callback))
  371. (defun biblio--selection-insert-quit ()
  372. "Insert BibTeX of current entry into source buffer and close results."
  373. (interactive)
  374. (biblio--selection-forward-bibtex #'biblio--selection-insert-callback t))
  375. (defun biblio--selection-metadata-at-point ()
  376. "Return the metadata of the entry at point."
  377. (or (get-text-property (point) 'biblio-metadata)
  378. (user-error "No entry at point")))
  379. (defun biblio--selection-forward-bibtex (forward-to &optional quit)
  380. "Retrieve BibTeX for entry at point and pass it to FORWARD-TO.
  381. If QUIT is set, also kill the results buffer."
  382. (let* ((metadata (biblio--selection-metadata-at-point))
  383. (results-buffer (current-buffer)))
  384. (progn
  385. (funcall (biblio-alist-get 'backend metadata)
  386. 'forward-bibtex metadata
  387. (lambda (bibtex)
  388. (with-current-buffer results-buffer
  389. (funcall forward-to (biblio-format-bibtex bibtex) metadata))))
  390. (when quit (quit-window)))))
  391. (defun biblio--selection-change-buffer (buffer-name)
  392. "Change buffer in which BibTeX results will be inserted.
  393. BUFFER-NAME is the name of the new target buffer."
  394. (interactive (list (read-buffer "Buffer to insert entries into: ")))
  395. (let ((buffer (get-buffer buffer-name)))
  396. (if (buffer-local-value 'buffer-read-only buffer)
  397. (user-error "%s is read-only" (buffer-name buffer))
  398. (setq biblio--target-buffer buffer))))
  399. (defvar biblio-selection-mode-actions-alist nil
  400. "An alist of extensions for `biblio-selection-mode'.
  401. Each element should be in the for (LABEL . FUNCTION); FUNCTION
  402. will be called with the metadata of the current item.")
  403. (defun biblio--completing-read-function ()
  404. "Return ido, unless user picked another completion package."
  405. (if (eq completing-read-function #'completing-read-default)
  406. #'ido-completing-read
  407. completing-read-function))
  408. (defun biblio-completing-read (prompt collection &optional predicate require-match
  409. initial-input hist def inherit-input-method)
  410. "Complete using `biblio-completing-read-function'.
  411. PROMPT, COLLECTION, PREDICATE, REQUIRE-MATCH, INITIAL-INPUT,
  412. HIST, DEF, INHERIT-INPUT-METHOD: see `completing-read'."
  413. (let ((completing-read-function (biblio--completing-read-function)))
  414. (completing-read prompt collection predicate require-match
  415. initial-input hist def inherit-input-method)))
  416. (defun biblio-completing-read-alist (prompt collection &optional predicate require-match
  417. initial-input hist def inherit-input-method)
  418. "Same as `biblio-completing-read', when COLLECTION in an alist.
  419. Complete with the `car's, and return the `cdr' of the result.
  420. PROMPT, COLLECTION, PREDICATE, REQUIRE-MATCH, INITIAL-INPUT,
  421. HIST, DEF, INHERIT-INPUT-METHOD: see `completing-read'."
  422. (let ((choices (mapcar #'car collection)))
  423. (cdr (assoc (biblio-completing-read
  424. prompt choices predicate require-match
  425. initial-input hist def inherit-input-method)
  426. collection))))
  427. (defun biblio--read-selection-extended-action ()
  428. "Read an action from `biblio-selection-mode-actions-alist'."
  429. (biblio-completing-read-alist
  430. "Action: " biblio-selection-mode-actions-alist nil t))
  431. (defun biblio--selection-extended-action (action)
  432. "Run an ACTION with metadata of current entry.
  433. Interactively, query for ACTION from
  434. `biblio-selection-mode-actions-alist'."
  435. (interactive (list (biblio--read-selection-extended-action)))
  436. (let* ((metadata (biblio--selection-metadata-at-point)))
  437. (funcall action metadata)))
  438. (defun biblio--selection-help ()
  439. "Show help on local keymap."
  440. (interactive)
  441. (biblio--help-with-major-mode))
  442. (defvar biblio-selection-mode-map
  443. (let ((map (make-sparse-keymap)))
  444. (define-key map (kbd "<up>") #'biblio--selection-previous)
  445. (define-key map (kbd "C-p") #'biblio--selection-previous)
  446. (define-key map (kbd "<down>") #'biblio--selection-next)
  447. (define-key map (kbd "C-n") #'biblio--selection-next)
  448. (define-key map (kbd "RET") #'biblio--selection-browse)
  449. (define-key map (kbd "<C-return>") #'biblio--selection-browse-direct)
  450. (define-key map (kbd "C-RET") #'biblio--selection-browse-direct)
  451. (define-key map (kbd "M-w") #'biblio--selection-copy)
  452. (define-key map (kbd "c") #'biblio--selection-copy)
  453. (define-key map (kbd "C-w") #'biblio--selection-copy-quit)
  454. (define-key map (kbd "C") #'biblio--selection-copy-quit)
  455. (define-key map (kbd "i") #'biblio--selection-insert)
  456. (define-key map (kbd "C-y") #'biblio--selection-insert-quit)
  457. (define-key map (kbd "I") #'biblio--selection-insert-quit)
  458. (define-key map (kbd "b") #'biblio--selection-change-buffer)
  459. (define-key map (kbd "x") #'biblio--selection-extended-action)
  460. (define-key map (kbd "?") #'biblio--selection-help)
  461. (define-key map (kbd "h") #'biblio--selection-help)
  462. (define-key map (kbd "q") #'quit-window)
  463. map)
  464. "Keybindings for Bibliographic search results.")
  465. (defconst biblio--selection-mode-name-base "Bibliographic search results")
  466. (defun biblio--selection-mode-name ()
  467. "Compute a modeline string for `biblio-selection-mode'."
  468. (concat biblio--selection-mode-name-base
  469. (if (bufferp biblio--target-buffer)
  470. (format " (→ %s)"
  471. (buffer-name biblio--target-buffer))
  472. "")))
  473. (define-derived-mode biblio-selection-mode fundamental-mode biblio--selection-mode-name-base
  474. "Browse bibliographic search results.
  475. \\{biblio-selection-mode-map}"
  476. (hl-line-mode)
  477. (visual-line-mode)
  478. (setq-local truncate-lines nil)
  479. (setq-local cursor-type nil)
  480. (setq-local buffer-read-only t)
  481. (setq-local mode-name '(:eval (biblio--selection-mode-name)))
  482. (setq-local
  483. header-line-format
  484. `(:eval
  485. (concat
  486. (ignore-errors
  487. (propertize " " 'display '(space :align-to 0) 'face 'fringe))
  488. (substitute-command-keys
  489. (biblio-join " "
  490. "\\[biblio--selection-help]: Help"
  491. "\\[biblio--selection-insert],\\[biblio--selection-insert-quit]: Insert BibTex"
  492. "\\[biblio--selection-copy],\\[biblio--selection-copy-quit]: Copy BibTeX"
  493. "\\[biblio--selection-extended-action]: Extended action"
  494. "\\[biblio--selection-browse]: Open in browser"
  495. "\\[biblio--selection-change-buffer]: Change buffer"))))))
  496. ;;; Printing search results
  497. (defun biblio-parenthesize (str)
  498. "Add parentheses to STR, if not empty."
  499. (if (seq-empty-p str) ""
  500. (concat "(" str ")")))
  501. (defun biblio-insert-with-prefix (prefix &rest strs)
  502. "Like INSERT with PREFIX and STRS, but set `wrap-prefix'.
  503. That is, the inserted text gets a `wrap-prefix' made of enough
  504. white space to align with the end of PREFIX."
  505. (declare (indent 1))
  506. (biblio--with-text-property 'wrap-prefix (make-string (length prefix) ?\s)
  507. (apply #'insert prefix strs)))
  508. (defface biblio-detail-header-face
  509. '((t :slant normal))
  510. "Face used for headers of details in `biblio-selection-mode'."
  511. :group 'biblio-faces)
  512. (defun biblio--insert-detail (prefix items newline)
  513. "Insert PREFIX followed by ITEMS, if ITEMS has non-empty entries.
  514. If ITEMS is a list or vector, join its entries with , . If
  515. NEWLINE is non-nil, add a newline before the main text."
  516. (when (or (vectorp items) (listp items))
  517. (setq items (biblio-join-1 ", " items)))
  518. (unless (seq-empty-p items)
  519. (when newline (insert "\n"))
  520. (let ((fontified (propertize prefix 'face 'biblio-detail-header-face)))
  521. (biblio-insert-with-prefix fontified items))))
  522. (defun biblio--nonempty-string-p (str)
  523. "Return STR if STR is non-empty."
  524. (unless (seq-empty-p str)
  525. str))
  526. (defun biblio--cleanup-field (text)
  527. "Cleanup TEXT for presentation to the user."
  528. (when text (biblio-strip (replace-regexp-in-string "[ \r\n\t]+" " " text))))
  529. (defun biblio--prepare-authors (authors)
  530. "Cleanup and join list of AUTHORS."
  531. (let* ((authors (biblio-remove-empty (seq-map #'biblio-strip authors)))
  532. (num-authors (length authors)))
  533. ;; Only truncate when significantly above limit
  534. (when (> num-authors (+ 2 biblio-authors-limit))
  535. (let* ((last (nthcdr biblio-authors-limit authors)))
  536. (setcar last (format "… (%d more)" (- num-authors biblio-authors-limit)))
  537. (setcdr last nil)))
  538. (if authors (biblio-join-1 ", " authors)
  539. "(no authors)")))
  540. (defun biblio--prepare-title (title &optional year)
  541. "Cleanup TITLE and add YEAR for presentation to the user."
  542. (concat (or (biblio--nonempty-string-p (biblio--cleanup-field title))
  543. "(no title)")
  544. (if year (format " [%s]" year) "")))
  545. (defun biblio--browse-url (button)
  546. "Open web browser on page pointed to by BUTTON."
  547. (browse-url (button-get button 'target)))
  548. (defun biblio-make-url-button (url &optional label)
  549. "Make a text button pointing to URL.
  550. With non-nil LABEL, use that instead of URL to label the button."
  551. (unless (seq-empty-p url)
  552. (with-temp-buffer
  553. (insert-text-button (or label url)
  554. 'target url
  555. 'follow-link t
  556. 'action #'biblio--browse-url)
  557. (buffer-string))))
  558. (defun biblio-insert-result (item &optional no-sep)
  559. "Print a (prepared) bibliographic search result ITEM.
  560. With NO-SEP, do not add space after the record.
  561. This command expects ITEM to be a single alist, in the following format:
  562. ((title . \"Title of entry\")
  563. (authors . (\"Author 1\" \"Author 2\" ))
  564. (container . \"Where this was published (which journal, conference, )\")
  565. (type . \"Type of document (journal paper, proceedings, report, )\")
  566. (category . \"Category of this document (aka primary topic)\")
  567. (publisher . \"Publisher of this document\")
  568. (references . \"Identifier(s) of this document (DOI, DBLP id, Handle, )\")
  569. (open-access-status . \"Open access status of this document\")
  570. (url . \"Relevant URL\")
  571. (year . \"Publication year as a string, if available\")
  572. (direct-url . \"Direct URL of paper (typically PDF)\"))
  573. Each of `container', `type', `category', `publisher',
  574. `references', and `open-access-status' may be a list; in that
  575. case, entries of the list are displayed comma-separated. All
  576. entries are optional.
  577. `crossref--extract-interesting-fields' and `dblp--extract-interesting-fields'
  578. provide examples of how to build such a result."
  579. (biblio--with-text-property 'biblio-metadata item
  580. (let-alist item
  581. (biblio-with-fontification 'font-lock-function-name-face
  582. (biblio-insert-with-prefix "> " (biblio--prepare-title .title .year)))
  583. (insert "\n")
  584. (biblio-with-fontification 'font-lock-doc-face
  585. (biblio-insert-with-prefix " " (biblio--prepare-authors .authors)))
  586. (biblio-with-fontification 'font-lock-comment-face
  587. (biblio--insert-detail " In: " .container t)
  588. (biblio--insert-detail " Type: " .type t)
  589. (biblio--insert-detail " Category: " .category t)
  590. (biblio--insert-detail " Publisher: " .publisher t)
  591. (biblio--insert-detail " References: " .references t)
  592. (biblio--insert-detail " Open Access: " .open-access-status t)
  593. (biblio--insert-detail " URL: " (list (biblio-make-url-button .url)
  594. (biblio-make-url-button .direct-url))
  595. t))
  596. (unless no-sep
  597. (insert "\n\n")))))
  598. (defface biblio-results-header-face
  599. '((t :height 1.5 :weight bold :inherit font-lock-preprocessor-face))
  600. "Face used for general search results header in `biblio-selection-mode'."
  601. :group 'biblio-faces)
  602. (defun biblio--search-results-header (&optional loading-p)
  603. "Compute a header for the current `selection-mode' buffer.
  604. With LOADING-P, mention that results are being loaded."
  605. (format "%s search results for %s%s"
  606. (funcall biblio--backend 'name)
  607. (biblio--quote biblio--search-terms)
  608. (if loading-p " (loading…)" "")))
  609. (defun biblio--make-results-buffer (target-buffer search-terms backend)
  610. "Set up the results buffer for TARGET-BUFFER, SEARCH-TERMS and BACKEND."
  611. (with-current-buffer (get-buffer-create
  612. (format "*%s search*" (funcall backend 'name)))
  613. (let ((inhibit-read-only t))
  614. (erase-buffer)
  615. (biblio-selection-mode)
  616. (setq biblio--target-buffer target-buffer)
  617. (setq biblio--search-terms search-terms)
  618. (setq biblio--backend backend)
  619. (biblio--insert-header (biblio--search-results-header t))
  620. (setq buffer-read-only t)
  621. (current-buffer))))
  622. (defun biblio--insert-header (header)
  623. "Prettify and insert HEADER in current buffer."
  624. (when header
  625. (biblio--with-text-property 'line-spacing 0.5
  626. (biblio--with-text-property 'line-height 1.75
  627. (biblio-with-fontification 'biblio-results-header-face
  628. (insert header "\n"))))))
  629. (defun biblio-insert-results (items &optional header)
  630. "Populate current buffer with ITEMS and HEADER, then display it."
  631. (let ((inhibit-read-only t))
  632. (erase-buffer)
  633. (biblio--insert-header header)
  634. (seq-do #'biblio-insert-result items))
  635. (pop-to-buffer (current-buffer))
  636. (biblio--selection-first)
  637. (hl-line-highlight))
  638. (defun biblio--tag-backend (backend items)
  639. "Add (backend . BACKEND) to each alist in ITEMS."
  640. (seq-map (lambda (i) (cons `(backend . ,backend) i)) items))
  641. (defun biblio--callback (results-buffer backend)
  642. "Generate a search results callback for RESULTS-BUFFER.
  643. Results are parsed with (BACKEND 'parse-buffer)."
  644. (biblio-generic-url-callback
  645. (lambda () ;; no allowed errors, so no arguments
  646. "Parse results of bibliographic search."
  647. (let ((results (biblio--tag-backend backend (funcall backend 'parse-buffer))))
  648. (with-current-buffer results-buffer
  649. (biblio-insert-results results (biblio--search-results-header)))
  650. (message "Tip: learn to browse results with `h'")))))
  651. ;;; Searching
  652. (defvar biblio--search-history nil)
  653. (defvar biblio-backends nil
  654. "List of biblio backends.
  655. This list is generally populated through `biblio-init-hook',
  656. which is called by `biblio-collect-backends'.
  657. Each backend is a function that take a variable number of
  658. arguments. The first argument is a command; the rest are
  659. arguments to this specific command. The command is one of the
  660. following:
  661. `name': (no arguments) The name of the backend, displayed when picking a
  662. backend from a list.
  663. `prompt': (no arguments) The string used when querying the user for a search
  664. term to feed this backend.
  665. `url': (one argument, QUERY) Create a URL to query the backend's API.
  666. `parse-buffer': (no arguments) Parse the contents of the current
  667. buffer and return a list of results. At the time of the call,
  668. the current buffer contains the results of querying a url
  669. returned by (THIS-BACKEND `url' QUERY). The format of individual
  670. results is described in the docstring of `biblio-insert-result').
  671. `forward-bibtex': (two arguments, METADATA and FORWARD-TO)
  672. Produce a BibTeX record from METADATA (one of the elements of the
  673. list produced by `parse-buffer') and call FORWARD-TO on it.
  674. For examples of backends, see one of `biblio-crossref-backend',
  675. `biblio-dblp-backend', `biblio-arxiv-backend', etc.
  676. To register your backend automatically, you may want to add a
  677. `register' command:
  678. `register': Add the current backend to `biblio-backends'.
  679. Something like (add-to-list \\='biblio-backends \\='THIS-BACKEND).
  680. Then it's enough to add your backend to `biblio-init-hook':
  681. ;;;###autoload
  682. \(add-hook \\='biblio-init-hook \\='YOUR-BACKEND-HERE).")
  683. (defvar biblio-init-hook nil
  684. "Hook run before every search.
  685. Each function is called with one argument, `register'. This
  686. makes it possible to register backends by adding them directly to
  687. this hook, and making them react to `register' by adding
  688. themselves to biblio-backends.")
  689. (defun biblio-collect-backends ()
  690. "Populate `biblio-backends' and return that."
  691. (run-hook-with-args 'biblio-init-hook 'register)
  692. biblio-backends)
  693. (defun biblio--named-backends ()
  694. "Collect an alist of (NAME . BACKEND)."
  695. (seq-map (lambda (b) (cons (funcall b 'name) b)) (biblio-collect-backends)))
  696. (defun biblio--read-backend ()
  697. "Run `biblio-init-hook', then read a backend from `biblio-backend'."
  698. (biblio-completing-read-alist "Backend: " (biblio--named-backends) nil t))
  699. (defun biblio--read-query (backend)
  700. "Interactively read a query.
  701. Get prompt string from BACKEND."
  702. (let* ((prompt (funcall backend 'prompt)))
  703. (read-string prompt nil 'biblio--search-history)))
  704. (defun biblio--lookup-1 (backend query)
  705. "Just like `biblio-lookup' on BACKEND and QUERY, but never prompt."
  706. (let ((results-buffer (biblio--make-results-buffer (current-buffer) query backend)))
  707. (biblio-url-retrieve
  708. (funcall backend 'url query)
  709. (biblio--callback results-buffer backend))
  710. results-buffer))
  711. ;;;###autoload
  712. (defun biblio-lookup (&optional backend query)
  713. "Perform a search using BACKEND, and QUERY.
  714. Prompt for any missing or nil arguments. BACKEND should be a
  715. function obeying the interface described in the docstring of
  716. `biblio-backends'. Returns the buffer in which results will be
  717. inserted."
  718. (interactive)
  719. (unless backend (setq backend (biblio--read-backend)))
  720. (unless query (setq query (biblio--read-query backend)))
  721. (biblio--lookup-1 backend query))
  722. (defun biblio-kill-buffers ()
  723. "Kill all `biblio-selection-mode' buffers."
  724. (interactive)
  725. (dolist (buf (buffer-list))
  726. (when (and (buffer-live-p buf)
  727. (eq (buffer-local-value 'major-mode buf)
  728. 'biblio-selection-mode))
  729. (kill-buffer buf))))
  730. ;; Local Variables:
  731. ;; nameless-current-name: "biblio"
  732. ;; checkdoc-arguments-in-order-flag: nil
  733. ;; End:
  734. (provide 'biblio-core)
  735. ;;; biblio-core.el ends here