;;; bibtex-completion.el --- A BibTeX backend for completion frameworks ;; Author: Titus von der Malsburg ;; Justin Burkett ;; Maintainer: Titus von der Malsburg ;; URL: https://github.com/tmalsburg/helm-bibtex ;; Package-Version: 20221024.857 ;; Package-Commit: 78f5931e1cc82e7ae2bcf0508cf31d0d1629a8dd ;; Version: 1.0.0 ;; Package-Requires: ((parsebib "1.0") (s "1.9.0") (dash "2.6.0") (f "0.16.2") (cl-lib "0.5") (biblio "0.2") (emacs "26.1")) ;; 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 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 this program. If not, see . ;;; Commentary: ;; A BibTeX backend for completion frameworks ;; There are currently two fronends: helm-bibtex and ivy-bibtex. ;; ;; See the github page for details: ;; ;; https://github.com/tmalsburg/helm-bibtex ;;; Code: (require 'browse-url) (require 'parsebib) (require 'cl-lib) (require 'dash) (require 's) (require 'f) (require 'biblio) (require 'filenotify) (require 'org-capture) ;; Silence byte-compiler (declare-function reftex-what-macro "reftex-parse") (declare-function reftex-get-bibfile-list "reftex-cite") (declare-function outline-show-all "outline") (declare-function org-narrow-to-subtree "org") (declare-function org-cycle-hide-drawers "org") (declare-function org-find-property "org") (declare-function org-show-entry "org") (declare-function org-entry-get "org") (declare-function org-element-parse-buffer "org-element") (declare-function org-element-map "org-element") (declare-function org-element-property "org-element") (defgroup bibtex-completion nil "Helm plugin for searching entries in a BibTeX bibliography." :group 'completion) (defcustom bibtex-completion-bibliography nil "The BibTeX file or list of BibTeX files. Org-bibtex users can also specify org mode bibliography files, in which case it will be assumed that a BibTeX file exists with the same name and extension bib instead of org. If the bib file has a different name, use a cons cell `(\"orgfile.org\" . \"bibfile.bib\")' instead." :group 'bibtex-completion :type '(choice file (repeat file))) ;;;###autoload (put 'bibtex-completion-bibliography 'safe-local-variable 'stringp) (defcustom bibtex-completion-library-path nil "A directory or list of directories in which PDFs are stored. Bibtex-completion assumes that the names of these PDFs are composed of the BibTeX-key plus a \".pdf\" suffix." :group 'bibtex-completion :type '(choice directory (repeat directory))) (defcustom bibtex-completion-pdf-open-function 'find-file "The function used for opening PDF files. This can be an arbitrary function that takes one argument: the path to the PDF file. The default is `find-file' which opens the PDF in Emacs (either with docview or, if installed, the much superior pdf-tools. When set to `helm-open-file-with-default-tool', the systems default viewer for PDFs is used." :group 'bibtex-completion :type 'function) (defcustom bibtex-completion-pdf-extension ".pdf" "The extension of a BibTeX entry's \"PDF\" file. This makes it possible to use another file type. It can also be a list of file types, which are then tried sequentially until a file is found. Beware that adding file types can reduce performance for large bibliographies. This variable has no effect if PDFs are referenced via the file field." :group 'bibtex-completion :type 'string) (defcustom bibtex-completion-find-additional-pdfs nil "If non-nil, all files whose base name starts with the BibTeX key and ends with `bibtex-completion-pdf-extension' are considered as PDFs, not only \".\". Note that for performance reasons, an entry is only marked as having a PDF if \". (length (car it)) 0) entry-alist) (cl-acons "" "" (gethash (downcase crossref) entry-hash))))) entry))))) (defun bibtex-completion-make-entry-hash (files reparsed-files) "Return a hash table of all potentially cross-referenced bibliography entries in FILES, assuming that only those files in REPARSED-FILES were reparsed whereas the other files in FILES were up-to-date. Only entries whose type belongs to `bibtex-completion-cross-referenced-entry-types' are included in the hash table." (cl-loop with entries = (cl-loop for file in files for entries = (cddr (assoc file bibtex-completion-cache)) if (member file reparsed-files) ;; Entries are alists of \(FIELD . VALUE\) pairs. append entries ;; Entries are \(STRING . ALIST\) conses. else append (mapcar 'cdr entries)) with ht = (make-hash-table :test #'equal :size (length entries)) for entry in entries for key = (bibtex-completion-get-value "=key=" entry) if (member (downcase (bibtex-completion-get-value "=type=" entry)) bibtex-completion-cross-referenced-entry-types) do (puthash (downcase key) entry ht) finally return ht)) (defun bibtex-completion-make-candidate (entry) "Return a candidate for ENTRY." (let* ((candidate (bibtex-completion-clean-string (s-join " " (-map #'cdr entry)))) (candidate (concat candidate " " (car (assoc "=has-pdf=" entry)))) (candidate (concat candidate " " (car (assoc "=has-note=" entry))))) (cons candidate entry))) (defun bibtex-completion-parse-bibliography (&optional ht-strings) "Parse the BibTeX entries listed in the current buffer and return a list of entries in the order in which they appeared in the BibTeX file. Also do some preprocessing of the entries. If HT-STRINGS is provided it is assumed to be a hash table." (goto-char (point-min)) (cl-loop with fields = (append '("title" "crossref") (-map (lambda (it) (if (symbolp it) (symbol-name it) it)) bibtex-completion-additional-search-fields)) for entry-type = (parsebib-find-next-item) while entry-type unless (member-ignore-case entry-type '("preamble" "string" "comment")) collect (let* ((entry (parsebib-read-entry entry-type (point) ht-strings)) (fields (append (list (if (assoc-string "author" entry 'case-fold) "author" "editor") (if (assoc-string "date" entry 'case-fold) "date" "year")) fields))) (-map (lambda (it) (cons (downcase (car it)) (cdr it))) (bibtex-completion-prepare-entry entry fields))))) (defun bibtex-completion-get-entry (entry-key) "Given a BibTeX key this function scans all bibliographies listed in `bibtex-completion-bibliography' and returns an alist of the record with that key. Fields from crossreferenced entries are appended to the requested entry." (let* ((entry (bibtex-completion-get-entry1 entry-key)) (crossref (bibtex-completion-get-value "crossref" entry)) (crossref (when crossref (bibtex-completion-get-entry1 crossref)))) (bibtex-completion-remove-duplicated-fields (append entry crossref)))) (defun bibtex-completion-get-entry1 (entry-key &optional do-not-find-pdf) (let ((bib (bibtex-completion-normalize-bibliography 'bibtex))) (with-temp-buffer (mapc #'insert-file-contents bib) (goto-char (point-min)) (if (re-search-forward (concat "^[ \t]*@\\(" parsebib--bibtex-identifier "\\)[[:space:]]*[\(\{][[:space:]]*" (regexp-quote entry-key) "[[:space:]]*,") nil t) (let ((entry-type (match-string 1))) (reverse (bibtex-completion-prepare-entry (parsebib-read-entry entry-type (point) bibtex-completion-string-hash-table) nil do-not-find-pdf))) (progn (display-warning :warning (concat "Bibtex-completion couldn't find entry with key \"" entry-key "\".")) nil))))) (defun bibtex-completion-find-pdf-in-field (key-or-entry) "Return the path of the PDF specified in the field `bibtex-completion-pdf-field' if that file exists. Returns nil if no file is specified, or if the specified file does not exist, or if `bibtex-completion-pdf-field' is nil." (when bibtex-completion-pdf-field (let* ((entry (if (stringp key-or-entry) (bibtex-completion-get-entry1 key-or-entry t) key-or-entry)) (value (bibtex-completion-get-value bibtex-completion-pdf-field entry))) (cond ((not value) nil) ; Field not defined. ((f-file? value) (list value)) ; A bare full path was found. ((-any 'f-file? (--map (f-join it (f-filename value)) (-flatten bibtex-completion-library-path))) (-filter 'f-file? (--map (f-join it (f-filename value)) (-flatten bibtex-completion-library-path)))) (t ; Zotero/Mendeley/JabRef/Calibre format: (let ((value (replace-regexp-in-string "\\([^\\]\\)[;,]" "\\1\^^" value))) (cl-loop ; Looping over the files: for record in (s-split "\^^" value) ; Replace unescaped colons by field separator: for record = (replace-regexp-in-string "\\([^\\]\\|^\\):" "\\1\^_" record) ; Unescape stuff: for record = (replace-regexp-in-string "\\\\\\(.\\)" "\\1" record) ; Now we can safely split: for record = (s-split "\^_" record) for file-name = (nth 0 record) for path = (or (nth 1 record) "") for paths = (if (s-match "^[A-Z]:" path) (list path) ; Absolute Windows path ; Something else: (append (list path file-name (f-join (f-root) path) ; Mendeley #105 (f-join (f-root) path file-name)) ; Mendeley #105 (--map (f-join it path) (-flatten bibtex-completion-library-path)) ; Jabref #100 (--map (f-join it path file-name) (-flatten bibtex-completion-library-path)))) ; Jabref #100 for result = (-first (lambda (path) (if (and (not (s-blank-str? path)) (f-exists? path)) path nil)) paths) if result collect result))))))) (defun bibtex-completion-find-pdf-in-library (key-or-entry &optional find-additional) "Searches the directories in `bibtex-completion-library-path' for a PDF whose name is composed of the BibTeX key plus `bibtex-completion-pdf-extension'. The path of the first matching PDF is returned. If FIND-ADDITIONAL is non-nil, the paths of all PDFs whose name starts with the BibTeX key and ends with `bibtex-completion-pdf-extension' are returned instead." (let* ((key (if (stringp key-or-entry) key-or-entry (bibtex-completion-get-value "=key=" key-or-entry))) (main-pdf (cl-loop for dir in (-flatten bibtex-completion-library-path) append (cl-loop for ext in (-flatten bibtex-completion-pdf-extension) collect (f-join dir (s-concat key ext)))))) (if find-additional (sort ; move main pdf on top of the list if needed (cl-loop for dir in (-flatten bibtex-completion-library-path) append (directory-files dir t (s-concat "^" (regexp-quote key) ".*\\(" (mapconcat 'regexp-quote (-flatten bibtex-completion-pdf-extension) "\\|") "\\)$"))) (lambda (x y) (and (member x main-pdf) (not (member y main-pdf))))) (-flatten (-first 'f-file? main-pdf))))) (defun bibtex-completion-find-pdf (key-or-entry &optional find-additional) "Return the path of the PDF associated with the specified entry KEY-OR-ENTRY. This is either the path(s) specified in the field `bibtex-completion-pdf-field' or, if that does not exist, the first PDF in any of the directories in `bibtex-completion-library-path' whose name is composed of the the BibTeX key plus `bibtex-completion-pdf-extension' (or if FIND-ADDITIONAL is non-nil, all PDFs in `bibtex-completion-library-path' whose name starts with the BibTeX key and ends with `bibtex-completion-pdf-extension'). Returns nil if no PDF is found." (or (bibtex-completion-find-pdf-in-field key-or-entry) (bibtex-completion-find-pdf-in-library key-or-entry find-additional))) (defun bibtex-completion-find-note-multiple-files (entry-key) "Find note file associated with entry ENTRY-KEY in the default directory. The default directory is `bibtex-completion-notes-path'. If the note file doesn’t exist, return nil." (and bibtex-completion-notes-path (f-directory? bibtex-completion-notes-path) (f-file? (f-join bibtex-completion-notes-path (s-concat entry-key bibtex-completion-notes-extension))))) (defun bibtex-completion-find-note-one-file (entry-key) "Find notes associated with entry ENTRY-KEY in the single notes file. The single notes file is the one specified in `bibtex-completion-notes-path'. If no note exists, return nil." (and bibtex-completion-notes-path (f-file? bibtex-completion-notes-path) (member entry-key bibtex-completion-cached-notes-keys))) ;; This defvar allows other packages like org-roam-bibtex to customize ;; the back-end for storing notes. (defvar bibtex-completion-find-note-functions (list #'bibtex-completion-find-note-multiple-files #'bibtex-completion-find-note-one-file) "List of functions to use to find note files. The functions should accept one argument: the key of the BibTeX entry and return non-nil if notes exist for that entry.") (defun bibtex-completion-prepare-entry (entry &optional fields do-not-find-pdf) "Prepare ENTRY for display. ENTRY is an alist representing an entry as returned by `parsebib-read-entry'. All the fields not in FIELDS are removed from ENTRY, with the exception of the \"=type=\" and \"=key=\" fields. If FIELDS is empty, all fields are kept. Also add a =has-pdf= and/or =has-note= field, if they exist for ENTRY. If DO-NOT-FIND-PDF is non-nil, this function does not attempt to find a PDF file." (when entry ; entry may be nil, in which case just return nil (let* ((fields (when fields (append fields (list "=type=" "=key=" "=has-pdf=" "=has-note=")))) ; Check for PDF: (entry (if (and (not do-not-find-pdf) (bibtex-completion-find-pdf entry)) (cons (cons "=has-pdf=" bibtex-completion-pdf-symbol) entry) entry)) (entry-key (cdr (assoc "=key=" entry))) ; Check for notes: (entry (if (cl-some #'identity (mapcar (lambda (fn) (funcall fn entry-key)) bibtex-completion-find-note-functions)) (cons (cons "=has-note=" bibtex-completion-notes-symbol) entry) entry)) ; Remove unwanted fields: (entry (if fields (--filter (member-ignore-case (car it) fields) entry) entry))) ;; Normalize case of entry type: (setcdr (assoc "=type=" entry) (downcase (cdr (assoc "=type=" entry)))) ;; Remove duplicated fields: (bibtex-completion-remove-duplicated-fields entry)))) (defun bibtex-completion-remove-duplicated-fields (entry) "Remove duplicated fields from ENTRY." (cl-remove-duplicates entry :test (lambda (x y) (string= (s-downcase x) (s-downcase y))) :key 'car :from-end t)) (defun bibtex-completion-format-entry (entry width) "Formats a BibTeX ENTRY for display in results list. WIDTH is the width of the results list. The display format is governed by the variable `bibtex-completion-display-formats'." (let* ((format (or (assoc-string (bibtex-completion-get-value "=type=" entry) bibtex-completion-display-formats-internal 'case-fold) (assoc t bibtex-completion-display-formats-internal))) (format-string (cadr format))) (s-format format-string (lambda (field) (let* ((field (split-string field ":")) (field-name (car field)) (field-width (cadr field)) (field-value (bibtex-completion-get-value field-name entry))) (when (and (string= field-name "author") (not field-value)) (setq field-value (bibtex-completion-get-value "editor" entry))) (when (and (string= field-name "year") (not field-value)) (setq field-value (car (split-string (bibtex-completion-get-value "date" entry "") "-")))) (setq field-value (bibtex-completion-clean-string (or field-value " "))) (when (member field-name '("author" "editor")) (setq field-value (bibtex-completion-shorten-authors field-value))) (if (not field-width) field-value (setq field-width (string-to-number field-width)) (truncate-string-to-width field-value (if (> field-width 0) field-width (- width (cddr format))) 0 ?\s))))))) (defun bibtex-completion-clean-string (s) "Remove quoting and superfluous white space from BibTeX field value in S." (if s (replace-regexp-in-string "[\n\t ]+" " " (replace-regexp-in-string "[\"{}]+" "" s)) nil)) (defun bibtex-completion-shorten-authors (authors) "Return a comma-separated list of the surnames in AUTHORS." (if authors (cl-loop for a in (s-split " and " authors) for p = (s-split "," a t) for sep = "" then ", " concat sep if (eq 1 (length p)) concat (-last-item (s-split " +" (car p) t)) else concat (car p)) nil)) (defun bibtex-completion-open-pdf (keys &optional fallback-action) "Open the PDFs associated with the marked entries using the function specified in `bibtex-completion-pdf-open-function'. If multiple PDFs are found for an entry, ask for the one to open using `completion-read'. If FALLBACK-ACTION is non-nil, it is called in case no PDF is found." (dolist (key keys) (let ((pdf (bibtex-completion-find-pdf key bibtex-completion-find-additional-pdfs))) (cond ((> (length pdf) 1) (let* ((pdf (f-uniquify-alist pdf)) (choice (completing-read "File to open: " (mapcar 'cdr pdf) nil t)) (file (car (rassoc choice pdf)))) (funcall bibtex-completion-pdf-open-function file))) (pdf (funcall bibtex-completion-pdf-open-function (car pdf))) (fallback-action (funcall fallback-action (list key))) (t (message "No PDF(s) found for this entry: %s" key)))))) (defun bibtex-completion-open-url-or-doi (keys) "Open the URL or DOI associated with entries in KEYS in a browser." (dolist (key keys) (let* ((entry (bibtex-completion-get-entry key)) (url (bibtex-completion-get-value "url" entry)) (doi (bibtex-completion-get-value "doi" entry)) (browse-url-browser-function (or bibtex-completion-browser-function browse-url-browser-function))) (if url (browse-url url) (if doi (browse-url (s-concat "http://dx.doi.org/" doi)) (message "No URL or DOI found for this entry: %s" key)))))) (defun bibtex-completion-open-any (keys) "Open the PDFs associated with the marked entries using the function specified in `bibtex-completion-pdf-open-function'. If multiple PDFs are found for an entry, ask for the one to open using `completion-read'. If no PDF is found, try to open a URL or DOI in the browser instead." (bibtex-completion-open-pdf keys 'bibtex-completion-open-url-or-doi)) (defun bibtex-completion-format-citation-default (keys) "Default formatter for keys, separates multiple keys in KEYS with commas." (s-join ", " keys)) (defvar bibtex-completion-cite-command-history nil "History list for LaTeX citation commands.") (defun bibtex-completion-format-citation-cite (keys) "Formatter for LaTeX citation commands. Prompts for the command and for arguments if the commands can take any. If point is inside or just after a citation command, only adds KEYS to it." (let (macro) (cond ((and (require 'reftex-parse nil t) (setq macro (reftex-what-macro 1)) (stringp (car macro)) (string-match "\\`\\\\cite\\|cite\\'" (car macro))) ;; We are inside a cite macro. Insert key at point, with appropriate delimiters. (delete-horizontal-space) (concat (pcase (preceding-char) (?\{ "") (?, " ") (_ ", ")) (s-join ", " keys) (if (member (following-char) '(?\} ?,)) "" ", "))) ((and (equal (preceding-char) ?\}) (require 'reftex-parse nil t) (save-excursion (forward-char -1) (setq macro (reftex-what-macro 1))) (stringp (car macro)) (string-match "\\`\\\\cite\\|cite\\'" (car macro))) ;; We are right after a cite macro. Append key and leave point at the end. (delete-char -1) (delete-horizontal-space t) (concat (pcase (preceding-char) (?\{ "") (?, " ") (_ ", ")) (s-join ", " keys) "}")) (t ;; We are not inside or right after a cite macro. Insert a full citation. (let* ((initial (when bibtex-completion-cite-default-as-initial-input bibtex-completion-cite-default-command)) (default (unless bibtex-completion-cite-default-as-initial-input bibtex-completion-cite-default-command)) (default-info (if default (format " (default \"%s\")" default) "")) (cite-command (completing-read (format "Cite command%s: " default-info) bibtex-completion-cite-commands nil nil initial 'bibtex-completion-cite-command-history default nil))) (if (member cite-command '("nocite" "supercite")) ; These don't want arguments. (format "\\%s{%s}" cite-command (s-join ", " keys)) (let ((prenote (if bibtex-completion-cite-prompt-for-optional-arguments (read-from-minibuffer "Prenote: ") "")) (postnote (if bibtex-completion-cite-prompt-for-optional-arguments (read-from-minibuffer "Postnote: ") ""))) (cond ((not (string= "" prenote)) (format "\\%s[%s][%s]{%s}" cite-command prenote postnote (s-join ", " keys))) ((not (string= "" postnote)) (format "\\%s[%s]{%s}" cite-command postnote (s-join ", " keys))) (t (format "\\%s{%s}" cite-command (s-join ", " keys))))))))))) (defun bibtex-completion-format-citation-pandoc-citeproc (keys) "Format pandoc-citeproc citations for the entries in KEYS." (let* ((prenote (if bibtex-completion-cite-prompt-for-optional-arguments (read-from-minibuffer "Prenote: ") "")) (postnote (if bibtex-completion-cite-prompt-for-optional-arguments (read-from-minibuffer "Postnote: ") "")) (prenote (if (string= "" prenote) "" (concat prenote " "))) (postnote (if (string= "" postnote) "" (concat ", " postnote)))) (format "[%s%s%s]" prenote (s-join "; " (--map (concat "@" it) keys)) postnote))) (defun bibtex-completion-format-citation-ebib (keys) "Format ebib references for keys in KEYS." (s-join ", " (--map (format "ebib:%s" it) keys))) (defun bibtex-completion-format-citation-sphinxcontrib-bibtex (keys) "Format sphinxcontrib-bibtex references for keys in KEYS." (format ":cite:`%s`" (s-join "," keys))) (defun bibtex-completion-format-citation-org-link-to-PDF (keys) "Format org-links to PDFs associated with entries in KEYS. Uses first matching PDF if several are available. Entries for which no PDF is available are omitted." (s-join ", " (cl-loop for key in keys for pdfs = (bibtex-completion-find-pdf key bibtex-completion-find-additional-pdfs) append (with-no-warnings (--map (org-make-link-string it key) pdfs))))) (defun bibtex-completion-format-citation-org-cite (keys) "Format org-links using Org mode's own cite syntax." (format "[cite:%s]" (s-join ";" (--map (format "@%s" it) keys)))) (defun bibtex-completion-format-citation-org-apa-link-to-PDF (keys) "Format org-links to PDF for entries in KEYS. Link text loosely follows APA format. Uses first matching PDF if several are available." (s-join ", " (cl-loop for key in keys for entry = (bibtex-completion-get-entry key) for author = (bibtex-completion-shorten-authors (or (bibtex-completion-get-value "author" entry) (bibtex-completion-get-value "editor" entry))) for year = (or (bibtex-completion-get-value "year" entry) (car (split-string (bibtex-completion-get-value "date" entry "") "-"))) for pdf = (car (bibtex-completion-find-pdf key)) if pdf collect (with-no-warnings (org-make-link-string pdf (format "%s (%s)" author year))) else collect (format "%s (%s)" author year)))) ;; When you want to create a todo list about reading, I think using ;; PDF's title is more intuitive. (defun bibtex-completion-format-citation-org-title-link-to-PDF (keys) "Formatter org-links to PDFs associated with entries in KEYS. Link text follows file title format. Uses first matching PDF if several are available." (s-join ", " (cl-loop for key in keys for entry = (bibtex-completion-get-entry key) for title = (bibtex-completion-apa-get-value "title" entry) for pdf = (or (car (bibtex-completion-find-pdf key)) (bibtex-completion-get-value "url" entry)) if pdf collect (with-no-warnings (org-make-link-string pdf title)) else collect (format "%s" title)))) (defun bibtex-completion-insert-citation (keys) "Insert citations for entries in KEYS at point. The format depends on `bibtex-completion-format-citation-functions'." (let ((format-function (cdr (or (assoc major-mode bibtex-completion-format-citation-functions) (assoc 'default bibtex-completion-format-citation-functions))))) (insert (funcall format-function keys)))) (defun bibtex-completion-insert-reference (keys) "Insert references for entries in KEYS." (let* ((refs (--map (s-word-wrap fill-column (concat "\n- " (bibtex-completion-apa-format-reference it))) keys))) (insert "\n" (s-join "\n" refs) "\n"))) (defun bibtex-completion-apa-format-reference (key) "Return a plain text reference in APA format for the publication specified by KEY." (let* ((entry (bibtex-completion-get-entry key)) (ref (pcase (downcase (bibtex-completion-get-value "=type=" entry)) ("article" (s-format "${author} (${year}). ${title}. ${journal}, ${volume}(${number}), ${pages}.${doi}" 'bibtex-completion-apa-get-value entry)) ("inproceedings" (s-format "${author} (${year}). ${title}. In ${editor}, ${booktitle} (pp. ${pages}). ${address}: ${publisher}." 'bibtex-completion-apa-get-value entry)) ("book" (s-format "${author} (${year}). ${title}. ${address}: ${publisher}." 'bibtex-completion-apa-get-value entry)) ("phdthesis" (s-format "${author} (${year}). ${title} (Doctoral dissertation). ${school}, ${address}." 'bibtex-completion-apa-get-value entry)) ("inbook" (s-format "${author} (${year}). ${chapter}. In ${editor} (Eds.), ${title} (pp. ${pages}). ${address}: ${publisher}." 'bibtex-completion-apa-get-value entry)) ("incollection" (s-format "${author} (${year}). ${title}. In ${editor} (Eds.), ${booktitle} (pp. ${pages}). ${address}: ${publisher}." 'bibtex-completion-apa-get-value entry)) ("proceedings" (s-format "${editor} (Eds.). (${year}). ${booktitle}. ${address}: ${publisher}." 'bibtex-completion-apa-get-value entry)) ("unpublished" (s-format "${author} (${year}). ${title}. Unpublished manuscript." 'bibtex-completion-apa-get-value entry)) (_ (s-format "${author} (${year}). ${title}." 'bibtex-completion-apa-get-value entry))))) (replace-regexp-in-string "\\([.?!]\\)\\." "\\1" ref))) ; Avoid sequences of punctuation marks. (defun bibtex-completion-apa-get-value (field entry &optional default) "Return FIELD or ENTRY formatted following the APA guidelines. Return DEFAULT if FIELD is not present in ENTRY. Return empty string if FIELD is not present in ENTRY and DEFAULT is nil." (or (pcase field ;; Virtual fields: ("author-or-editor" ;; Avoid if-let and when-let because they're not working reliably ;; in all versions of Emacs that we currently support: (if-let ((value (bibtex-completion-get-value "author" entry))) (bibtex-completion-apa-format-authors value) (when-let ((value (bibtex-completion-get-value "editor" entry))) (bibtex-completion-apa-format-editors value)))) ("author-or-editor-abbrev" (if-let ((value (bibtex-completion-get-value "author" entry))) (bibtex-completion-apa-format-authors-abbrev value) (when-let ((value (bibtex-completion-get-value "editor" entry))) (bibtex-completion-apa-format-editors-abbrev value)))) ("author-abbrev" (when-let ((value (bibtex-completion-get-value "author" entry))) (bibtex-completion-apa-format-authors-abbrev value))) ("editor-abbrev" (when-let ((value (bibtex-completion-get-value "editor" entry))) (bibtex-completion-apa-format-editors-abbrev value))) (_ ;; Real fields: (let ((value (bibtex-completion-get-value field entry))) (if value (pcase field ;; https://owl.english.purdue.edu/owl/resource/560/06/ ("author" (bibtex-completion-apa-format-authors value)) ("editor" (bibtex-completion-apa-format-editors value)) ;; When referring to books, chapters, articles, or Web pages, ;; capitalize only the first letter of the first word of a ;; title and subtitle, the first word after a colon or a dash ;; in the title, and proper nouns. Do not capitalize the first ;; letter of the second word in a hyphenated compound word. ("title" (replace-regexp-in-string ; remove braces "[{}]" "" (replace-regexp-in-string ; remove macros "\\\\[[:alpha:]]+{" "" (replace-regexp-in-string ; upcase initial letter "^[[:alpha:]]" 'upcase (replace-regexp-in-string ; preserve stuff in braces from being downcased "\\(^[^{]*{\\)\\|\\(}[^{]*{\\)\\|\\(}.*$\\)\\|\\(^[^{}]*$\\)" (lambda (x) (downcase (s-replace "\\" "\\\\" x))) value))))) ("booktitle" value) ;; Maintain the punctuation and capitalization that is used by ;; the journal in its title. ("pages" (s-join "–" (s-split "[^0-9]+" value t))) ("doi" (s-concat " http://dx.doi.org/" value)) ("year" (or value (car (split-string (bibtex-completion-get-value "date" entry "") "-")))) (_ value)))))) default "")) (defun bibtex-completion-apa-format-authors (value &optional abbrev) "Format author list in VALUE in APA style. When ABBREV is non-nil, format in abbreviated APA style instead." (cl-loop for a in (s-split " and " value t) if (s-index-of "{" a) collect (replace-regexp-in-string "[{}]" "" a) into authors else if (s-index-of "," a) collect (let ((p (s-split " *, *" a t))) (concat (car p) ", " (s-join " " (-map (lambda (it) (concat (s-left 1 it) ".")) (s-split " " (cadr p)))))) into authors else collect (let ((p (s-split " " a t))) (concat (-last-item p) ", " (s-join " " (-map (lambda (it) (concat (s-left 1 it) ".")) (-butlast p))))) into authors finally return (let ((l (length authors))) (cond ((= l 1) (car authors)) ((and abbrev (= l 2)) (concat (s-join " & " authors))) (abbrev (format "%s et al." (car authors))) ((< l 8) (concat (s-join ", " (-butlast authors)) ", & " (-last-item authors))) (t (concat (s-join ", " (-slice authors 0 7)) ", …")))))) (defun bibtex-completion-apa-format-authors-abbrev (value) "Format author list in VALUE in abbreviated APA style." (bibtex-completion-apa-format-authors value t)) (defun bibtex-completion-apa-format-editors (value &optional abbrev) "Format editors in VALUE in APA style. When ABBREV is non-nil, format in abbreviated APA style instead." (cl-loop for a in (s-split " and " value t) if (s-index-of "," a) collect (let ((p (s-split " *, *" a t))) (concat (s-join " " (-map (lambda (it) (concat (s-left 1 it) ".")) (s-split " " (cadr p)))) " " (car p))) into editors else collect (let ((p (s-split " " a t))) (concat (s-join " " (-map (lambda (it) (concat (s-left 1 it) ".")) (-butlast p))) " " (-last-item p))) into editors finally return (let ((l (length editors))) (cond ((= l 1) (car editors)) ((and abbrev (= l 2)) (concat (s-join " & " editors))) (abbrev (format "%s et al." (car editors))) ((< l 8) (concat (s-join ", " (-butlast editors)) ", & " (-last-item editors))) (t (concat (s-join ", " (-slice editors 0 7)) ", …")))))) (defun bibtex-completion-apa-format-editors-abbrev (value) "Format editor list in VALUE in abbreviated APA style." (bibtex-completion-apa-format-editors value t)) (defun bibtex-completion-get-value (field entry &optional default) "Return the value for FIELD in ENTRY or DEFAULT if the value is not defined. Surrounding curly braces are stripped." (let ((value (cdr (assoc-string field entry 'case-fold)))) (if value (replace-regexp-in-string "\\(^[[:space:]]*[\"{][[:space:]]*\\)\\|\\([[:space:]]*[\"}][[:space:]]*$\\)" "" ;; Collapse whitespaces when the content is not a path: (if (equal bibtex-completion-pdf-field field) value (s-collapse-whitespace value))) default))) (defun bibtex-completion-insert-key (keys) "Insert BibTeX KEYS at point." (insert (funcall 'bibtex-completion-format-citation-default keys))) (defun bibtex-completion-insert-bibtex (keys) "Insert BibTeX entries for entries in KEYS at point." (insert (s-join "\n" (--map (bibtex-completion-make-bibtex it) keys)))) (defun bibtex-completion-make-bibtex (key) "Create a self-contained BibTeX entry KEY. Self-contained means that cross-referenced entries are merged." (let* ((entry (bibtex-completion-get-entry key)) (entry-type (bibtex-completion-get-value "=type=" entry))) (format "@%s{%s,\n%s}\n" entry-type key (cl-loop for field in entry for name = (car field) for value = (cdr field) unless (member name (append (-map (lambda (it) (if (symbolp it) (symbol-name it) it)) bibtex-completion-no-export-fields) '("=type=" "=key=" "=has-pdf=" "=has-note=" "crossref"))) concat (format " %s = {%s},\n" name value))))) (defun bibtex-completion-add-PDF-attachment (keys) "Attach the PDFs of the entries with the given KEYS where available." (dolist (key keys) (let ((pdf (bibtex-completion-find-pdf key bibtex-completion-find-additional-pdfs))) (if pdf (mapc 'mml-attach-file pdf) (message "No PDF(s) found for this entry: %s" key))))) (define-minor-mode bibtex-completion-notes-mode "Minor mode for managing notes." :keymap (let ((map (make-sparse-keymap))) (define-key map (kbd "C-c C-c") 'bibtex-completion-exit-notes-buffer) (define-key map (kbd "C-c C-w") 'org-refile) map) (setq-local header-line-format (substitute-command-keys " Finish \\[bibtex-completion-exit-notes-buffer], refile \\[org-refile]"))) ;; Define global minor mode. This is needed to the toggle minor mode. ;;;###autoload (define-globalized-minor-mode bibtex-completion-notes-global-mode bibtex-completion-notes-mode bibtex-completion-notes-mode) (defun bibtex-completion-exit-notes-buffer () "Exit notes buffer and delete its window. This will also disable `bibtex-completion-notes-mode' and remove the header line." (interactive) (widen) (bibtex-completion-notes-global-mode -1) (setq-local header-line-format nil) (save-buffer) (let ((window (get-buffer-window (get-file-buffer bibtex-completion-notes-path)))) (if (and window (not (one-window-p window))) (delete-window window) (switch-to-buffer (other-buffer))))) (defun bibtex-completion-fill-template (entry template) "Fill TEMPLATE according to info from ENTRY. First, the BibTeX fields are expanded (e.g. ${field-name}). Then, the `org-capture' %-escapes are replaced with their values according to `org-capture-templates'." (let ((bibtex-exp (s-format template 'bibtex-completion-apa-get-value entry))) ;; Delete trailing newline inserted by `org-capture-fill-template' (substring (->> bibtex-exp ;; Escape newlines to prevent `org-capture-fill-template' from ;; gobbling them (replace-regexp-in-string "\n" "\\\\n") (org-capture-fill-template) ;; Restore newlines (replace-regexp-in-string "\\\\n" "\n")) 0 -1))) ;; The purpose of this defvar is to allow other packages like ;; org-roam-bibtex to customize the back-end used for notes. (defvar bibtex-completion-edit-notes-function #'bibtex-completion-edit-notes-default "Function used to edit notes. The function should accept one argument, a list of BibTeX keys.") ;; TODO Split this function into two, one for one file per note and ;; the other for one file for all notes. (defun bibtex-completion-edit-notes-default (keys) "Open the notes associated with the entries in KEYS. Creates new notes where none exist yet." (dolist (key keys) (let* ((entry (bibtex-completion-get-entry key)) (year (or (bibtex-completion-get-value "year" entry) (car (split-string (bibtex-completion-get-value "date" entry "") "-")))) (entry (push (cons "year" year) entry))) (if (and bibtex-completion-notes-path (f-directory? bibtex-completion-notes-path)) ; One notes file per publication: (let* ((path (f-join bibtex-completion-notes-path (s-concat key bibtex-completion-notes-extension)))) (find-file path) (unless (f-exists? path) ;; First expand BibTeX variables, then org-capture template vars: (insert (bibtex-completion-fill-template entry bibtex-completion-notes-template-multiple-files)))) ; One file for all notes: (unless (and buffer-file-name (f-same? bibtex-completion-notes-path buffer-file-name)) (find-file-other-window bibtex-completion-notes-path)) (widen) (outline-show-all) (goto-char (point-min)) (if (re-search-forward (format bibtex-completion-notes-key-pattern (regexp-quote key)) nil t) ; Existing entry found: (when (eq major-mode 'org-mode) (org-narrow-to-subtree) (re-search-backward "^\*+ " nil t) (org-cycle-hide-drawers nil) (bibtex-completion-notes-mode 1)) ; Create a new entry: (goto-char (point-max)) (save-excursion (insert (bibtex-completion-fill-template entry bibtex-completion-notes-template-one-file))) (re-search-forward "^*+ " nil t)) (when (eq major-mode 'org-mode) (org-narrow-to-subtree) (re-search-backward "^\*+ " nil t) (org-cycle-hide-drawers nil) (goto-char (point-max)) (bibtex-completion-notes-mode 1)) ;; Move point to ‘%?’ if it’s included in the pattern (when (save-excursion (progn (goto-char (point-min)) (re-search-forward "%\\?" nil t))) (let ((beginning (match-beginning 0)) (end (match-end 0))) (delete-region beginning end) (goto-char beginning))))))) (defun bibtex-completion-edit-notes (keys) "Open the notes associated with KEYS using `bibtex-completion-edit-notes-function'." (funcall bibtex-completion-edit-notes-function keys)) (defun bibtex-completion-show-entry (keys) "Show the first entry in KEYS in the relevant BibTeX file." (catch 'break (dolist (bib-file (bibtex-completion-normalize-bibliography 'main)) (let ((key (car keys)) (buf (or (get-file-buffer bib-file) (find-buffer-visiting bib-file)))) (find-file bib-file) (widen) (if (eq major-mode 'org-mode) (let* ((prop (if (boundp 'org-bibtex-key-property) org-bibtex-key-property "CUSTOM_ID")) (match (org-find-property prop key))) (when match (goto-char match) (org-show-entry) (throw 'break t))) (goto-char (point-min)) (when (re-search-forward (concat "^@\\(" parsebib--bibtex-identifier "\\)[[:space:]]*[\(\{][[:space:]]*" (regexp-quote key) "[[:space:]]*,") nil t) (throw 'break t))) (unless buf (kill-buffer)))))) (defun bibtex-completion-add-pdf-to-library (keys) "Add a PDF to the library for the first entry in KEYS. The PDF can be added either from an open buffer, a file, or a URL." (let* ((key (car keys)) (source (char-to-string (read-char-choice "Add pdf from [b]uffer, [f]ile, or [u]rl? " '(?b ?f ?u)))) (buffer (when (string= source "b") (read-buffer-to-switch "Add pdf buffer: "))) (file (when (string= source "f") (expand-file-name (read-file-name "Add pdf file: " nil nil t)))) (url (when (string= source "u") (read-string "Add pdf URL: "))) (path (-flatten (list bibtex-completion-library-path))) (path (if (cdr path) (completing-read "Add pdf to: " path nil t) (car path))) (pdf (expand-file-name (completing-read "Rename pdf to: " (--map (s-concat key it) (-flatten bibtex-completion-pdf-extension)) nil nil key) path))) (cond (buffer (with-current-buffer buffer (write-file pdf t))) (file (copy-file file pdf 1)) (url (url-copy-file url pdf 1))))) (defun bibtex-completion-fallback-action (url-or-function search-expression) "Execute fallback option. Could consist of opening an URL or executing a function, depending of type of URL-OR-FUNCTION (`stringp' or `function'). If string, SEARCH-EXPRESSION will be inserted at %s in string. If function, it will be called with SEARCH-EXPRESSION as argument." (let ((browse-url-browser-function (or bibtex-completion-browser-function browse-url-browser-function))) (cond ((stringp url-or-function) (browse-url (format url-or-function (url-hexify-string search-expression)))) ((functionp url-or-function) (funcall url-or-function search-expression)) (t (error "Don't know how to interpret this: %s" url-or-function))))) (defun bibtex-completion-fallback-candidates () "Compile list of fallback options. These consist of the online resources defined in `bibtex-completion-fallback-options' plus one entry for each bibliography file that will open that file for editing." (let ((bib-files (bibtex-completion-normalize-bibliography 'main))) (-concat (--map (cons (s-concat "Create new entry in " (f-filename it)) `(lambda (_search-expression) (find-file ,it) (goto-char (point-max)) (newline))) bib-files) bibtex-completion-fallback-options))) (defun bibtex-completion-find-local-bibliography () "Return a list of BibTeX files associated with the current file. If the current file is a BibTeX file, return this file. In LaTeX documents, use `reftex' to find associated BibTeX files. In org files return the local or global org bibliography (see oc.el). If all fails, return nil." (or (and (buffer-file-name) (string= (or (f-ext (buffer-file-name)) "") "bib") (list (buffer-file-name))) ;; LaTeX: (and (buffer-file-name) (string= (or (f-ext (buffer-file-name)) "") "tex") (require 'reftex-cite nil t) (ignore-errors (reftex-get-bibfile-list))) ;; Org (with oc.el): (and (buffer-file-name) (string= (or (f-ext (buffer-file-name)) "") "org") (fboundp 'org-cite-list-bibliography-files) (org-cite-list-bibliography-files)))) (defun bibtex-completion-get-key-bibtex () "Return the key of the BibTeX entry at point, nil otherwise. This function can be used by `bibtex-completion-key-at-point' to find the key of the BibTeX entry at point in a BibTeX-mode buffer." (when (eq major-mode 'bibtex-mode) (save-excursion (bibtex-beginning-of-entry) (and (looking-at bibtex-entry-maybe-empty-head) (bibtex-key-in-head))))) (defun bibtex-completion-get-key-latex () "Return the key of the BibTeX entry at point, nil otherwise. This function can be used by `bibtex-completion-key-at-point' to find the key of the BibTeX entry at point in a LaTeX buffer." (when (and (derived-mode-p 'latex-mode) (require 'reftex-parse nil t)) (save-excursion (skip-chars-backward "[:space:],;}") (let ((macro (reftex-what-macro 1))) (and (stringp (car macro)) (string-match "\\`\\\\cite\\|cite\\'" (car macro)) ;; allow '_' in citekeys (let ((temp-syn-table (make-syntax-table))) (modify-syntax-entry ?_ "_" temp-syn-table) (with-syntax-table temp-syn-table (thing-at-point 'symbol)))))))) (defun bibtex-completion-get-key-org-bibtex () "Return the key of the BibTeX entry at point, nil otherwise. This function can be used by `bibtex-completion-key-at-point' to find the key of the BibTeX entry at point in an Org-mode buffer." (when (eq major-mode 'org-mode) (let (key) (and (setq key (org-entry-get nil (if (boundp 'org-bibtex-key-property) org-bibtex-key-property "CUSTOM_ID") t)) ;; KEY may be the empty string the the property is ;; present but has no value (> (length key) 0) key)))) (defun bibtex-completion-get-key-org-cite () "Return the org-cite key at point, nil otherwise. This function can be used by `bibtex-completion-key-at-point' to find the org-cite key at point in an Org-mode buffer." (when (eq major-mode 'org-mode) (let ((el (org-element-context))) (when (eq (car el) 'citation-reference) (plist-get (cadr el) :key))))) (defvar bibtex-completion-key-at-point-functions (list #'bibtex-completion-get-key-bibtex #'bibtex-completion-get-key-latex #'bibtex-completion-get-key-org-bibtex #'bibtex-completion-get-key-org-cite) "List of functions to use to find the BibTeX key. The functions should take no argument and return the BibTeX key. Stops as soon as a function returns something. See `bibtex-completion-key-at-point' for details.") (defun bibtex-completion-key-at-point () "Return the key of the BibTeX entry at point. The functions used to match the keys are defined in `bibtex-completion-key-at-point-functions'." (cl-some #'identity (mapcar #'funcall bibtex-completion-key-at-point-functions))) (provide 'bibtex-completion) ;; Local Variables: ;; byte-compile-warnings: (not cl-functions obsolete) ;; coding: utf-8 ;; indent-tabs-mode: nil ;; End: ;;; bibtex-completion.el ends here