;;; bibtex-completion.el --- A BibTeX backend for completion frameworks ;; Author: Titus von der Malsburg ;; Justin Burkett ;; Maintainer: Titus von der Malsburg ;; 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")) ;; 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) ;; 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))) (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." (cons (bibtex-completion-clean-string (s-join " " (-map #'cdr entry))) 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) "Returns 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 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) "Returns the path of the PDF associated with the specified 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-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 (or ;; One note file per entry: (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)))) ;; All notes in one file: (and bibtex-completion-notes-path (f-file? bibtex-completion-notes-path) (member entry-key bibtex-completion-cached-notes-keys))) (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) "Removes quoting and superfluous white space from BibTeX field values." (if s (replace-regexp-in-string "[\n\t ]+" " " (replace-regexp-in-string "[\"{}]+" "" s)) nil)) (defun bibtex-completion-shorten-authors (authors) "Returns 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 associated URL or DOI 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 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) "Formatter for pandoc-citeproc citations." (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) "Formatter for ebib references." (s-join ", " (--map (format "ebib:%s" it) keys))) (defun bibtex-completion-format-citation-org-link-to-PDF (keys) "Formatter for org-links to PDF. 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 (--map (format "[[%s][%s]]" it key) pdfs)))) (defun bibtex-completion-format-citation-org-apa-link-to-PDF (keys) "Formatter for org-links to PDF. 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 (format "[[file:%s][%s (%s)]]" pdf 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 for org-links to PDF. 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-get-value "title" entry) for pdf = (or (car (bibtex-completion-find-pdf key)) (bibtex-completion-get-value "url" entry)) if pdf collect (format "[[file:%s][%s]]" pdf title) else collect (format "%s" title)))) (defun bibtex-completion-insert-citation (keys) "Insert citation 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 a reference for each selected entry." (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) "Returns 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}). ${title}. In ${editor} (Eds.), ${booktitle} (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." ;; Virtual fields: (cond ((string= field "author-or-editor") (let ((value (bibtex-completion-get-value "author" entry))) (if value (bibtex-completion-apa-format-authors value) (bibtex-completion-apa-format-editors (bibtex-completion-get-value "editor" entry))))) ((string= field "author-abbrev") (let ((value (bibtex-completion-get-value "author" entry))) (bibtex-completion-apa-format-authors-abbrev value))) (t ;; 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)) ""))))) (defun bibtex-completion-apa-format-authors (value) (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)) ((< 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) (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)) ((= l 2) (concat (s-join " & " authors))) (t (format "%s et al." (car authors))))))) (defun bibtex-completion-apa-format-editors (value) (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 authors 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 authors finally return (let ((l (length authors))) (cond ((= l 1) (car authors)) ((< l 8) (concat (s-join ", " (-butlast authors)) ", & " (-last-item authors))) (t (concat (s-join ", " authors) ", ...")))))) (defun bibtex-completion-get-value (field entry &optional default) "Return the requested value 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:]]*$\\)" "" (s-collapse-whitespace value)) default))) (defun bibtex-completion-insert-key (keys) "Insert BibTeX key at point." (insert (funcall 'bibtex-completion-format-citation-default keys))) (defun bibtex-completion-insert-bibtex (keys) "Insert BibTeX key at point." (insert (s-join "\n" (--map (bibtex-completion-make-bibtex it) keys)))) (defun bibtex-completion-make-bibtex (key) (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 selected entries 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. (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-edit-notes (keys) "Open the notes associated with the selected entries using `find-file'." (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) (insert (s-format bibtex-completion-notes-template-multiple-files 'bibtex-completion-apa-get-value entry)))) ; 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 (s-format bibtex-completion-notes-template-one-file 'bibtex-completion-apa-get-value entry))) (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)))))) (defun bibtex-completion-buffer-visiting (file) (or (get-file-buffer file) (find-buffer-visiting file))) (defun bibtex-completion-show-entry (keys) "Show the first selected entry in the BibTeX file." (catch 'break (dolist (bib-file (bibtex-completion-normalize-bibliography 'main)) (let ((key (car keys)) (buf (bibtex-completion-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 selected entry. 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) (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. Otherwise, try to use `reftex' to find the associated BibTeX files. If this fails, return nil." (or (and (buffer-file-name) (string= (or (f-ext (buffer-file-name)) "") "bib") (list (buffer-file-name))) (and (buffer-file-name) (require 'reftex-cite nil t) (ignore-errors (reftex-get-bibfile-list))))) (defun bibtex-completion-key-at-point () "Return the key of the BibTeX entry at point. If the current file is a BibTeX file, return the key of the entry at point. Otherwise, try to use `reftex' to check whether point is at a citation macro, and if so return the key at point. Otherwise, if the current file is an org-mode file, return the value of `org-bibtex-key-property' (or default to \"CUSTOM_ID\"). Otherwise, return nil." (or (and (eq major-mode 'bibtex-mode) (save-excursion (bibtex-beginning-of-entry) (and (looking-at bibtex-entry-maybe-empty-head) (bibtex-key-in-head)))) (and (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))))))) (and (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))))) (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