1434 lines
65 KiB
EmacsLisp
1434 lines
65 KiB
EmacsLisp
;;; bibtex-completion.el --- A BibTeX backend for completion frameworks
|
||
|
||
;; Author: Titus von der Malsburg <malsburg@posteo.de>
|
||
;; Justin Burkett <justin@burkett.cc>
|
||
;; Maintainer: Titus von der Malsburg <malsburg@posteo.de>
|
||
;; 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 <http://www.gnu.org/licenses/>.
|
||
|
||
;;; 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 'org-element)
|
||
(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")
|
||
|
||
(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 \"<key>.<extension>\". Note that
|
||
for performance reasons, an entry is only marked as having a
|
||
PDF if \"<key>.<extension\" exists."
|
||
:group 'bibtex-completion
|
||
:type 'boolean)
|
||
|
||
(defcustom bibtex-completion-pdf-symbol "⌘"
|
||
"Symbol used to indicate that a PDF file is available for a
|
||
publication. This should be a single character."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-format-citation-functions
|
||
'((org-mode . bibtex-completion-format-citation-ebib)
|
||
(latex-mode . bibtex-completion-format-citation-cite)
|
||
(markdown-mode . bibtex-completion-format-citation-pandoc-citeproc)
|
||
(default . bibtex-completion-format-citation-default))
|
||
"The functions used for formatting citations. The publication
|
||
can be cited, for example, as \cite{key} or ebib:key depending on
|
||
the major mode of the current buffer. Note that the functions
|
||
should accept a list of keys as input. With multiple marked
|
||
entries one can insert multiple keys at once,
|
||
e.g. \cite{key1,key2}. See the functions
|
||
`bibtex-completion-format-citation-ebib' and
|
||
`bibtex-completion-format-citation-cite' as examples."
|
||
:group 'bibtex-completion
|
||
:type '(alist :key-type symbol :value-type function))
|
||
|
||
(defcustom bibtex-completion-notes-path nil
|
||
"The place where notes are stored. This is either a file, in
|
||
which case all notes are stored in that file, or a directory, in
|
||
which case each publication gets its own notes file in that
|
||
directory. In the latter case, bibtex-completion assumes that the
|
||
names of the note files are composed of the BibTeX-key plus a
|
||
suffix that is specified in `bibtex-completion-notes-extension'."
|
||
:group 'bibtex-completion
|
||
:type '(choice file directory (const nil)))
|
||
|
||
(defcustom bibtex-completion-notes-template-multiple-files
|
||
"#+TITLE: Notes on: ${author-or-editor} (${year}): ${title}\n\n"
|
||
"Template used to create a new note when each note is stored in
|
||
a separate file. '${field-name}' can be used to insert the value
|
||
of a BibTeX field into the template. Apart from the fields defined in
|
||
the entry, one can also use the virtual field `author-or-editor` which
|
||
contains the author names if defined and otherwise the names of the
|
||
editors."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-notes-template-one-file
|
||
"\n* ${author-or-editor} (${year}): ${title}\n :PROPERTIES:\n :Custom_ID: ${=key=}\n :END:\n\n"
|
||
"Template used to create a new note when all notes are stored
|
||
in one file. '${field-name}' can be used to insert the value of
|
||
a BibTeX field into the template. Apart from the fields defined in
|
||
the entry, one can also use the virtual field `author-or-editor` which
|
||
contains the author names if defined and otherwise the names of the
|
||
editors."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-notes-key-pattern
|
||
":Custom_ID: +%s\\( \\|$\\)"
|
||
"The pattern used to find entries in the notes file. Only
|
||
relevant when all notes are stored in one file. The key can be
|
||
inserted into the pattern using the `format` function."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-notes-extension ".org"
|
||
"The extension of the files containing notes. This is only
|
||
used when `bibtex-completion-notes-path' is a directory (not a file)."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-notes-symbol "✎"
|
||
"Symbol used to indicate that a publication has notes. This
|
||
should be a single character."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-fallback-options
|
||
'(("CrossRef (biblio.el)"
|
||
. (lambda (search-expression) (biblio-lookup #'biblio-crossref-backend search-expression)))
|
||
("arXiv (biblio.el)"
|
||
. (lambda (search-expression) (biblio-lookup #'biblio-arxiv-backend search-expression)))
|
||
("DBLP (computer science bibliography) (biblio.el)"
|
||
. (lambda (search-expression) (biblio--lookup-1 #'biblio-dblp-backend search-expression)))
|
||
("HAL (French open archive) (biblio.el)"
|
||
. (lambda (search-expression) (biblio--lookup-1 #'biblio-hal-backend search-expression)))
|
||
("Google Scholar (web)"
|
||
. "https://scholar.google.co.uk/scholar?q=%s")
|
||
("Pubmed (web)"
|
||
. "https://www.ncbi.nlm.nih.gov/pubmed/?term=%s")
|
||
("Bodleian Library (web)"
|
||
. "http://solo.bodleian.ox.ac.uk/primo_library/libweb/action/search.do?vl(freeText0)=%s&fn=search&tab=all")
|
||
("Library of Congress (web)"
|
||
. "https://www.loc.gov/search/?q=%s&all=true&st=list")
|
||
("Deutsche Nationalbibliothek (web)"
|
||
. "https://portal.dnb.de/opac.htm?query=%s")
|
||
("British National Library (web)"
|
||
. "http://explore.bl.uk/primo_library/libweb/action/search.do?&vl(freeText0)=%s&fn=search")
|
||
("Bibliothèque nationale de France (web)"
|
||
. "http://catalogue.bnf.fr/servlet/RechercheEquation?host=catalogue?historique1=Recherche+par+mots+de+la+notice&niveau1=1&url1=/jsp/recherchemots_simple.jsp?host=catalogue&maxNiveau=1&categorieRecherche=RechercheMotsSimple&NomPageJSP=/jsp/recherchemots_simple.jsp?host=catalogue&RechercheMotsSimpleAsauvegarder=0&ecranRechercheMot=/jsp/recherchemots_simple.jsp&resultatsParPage=20&x=40&y=22&nbElementsHDJ=6&nbElementsRDJ=7&nbElementsRCL=12&FondsNumerise=M&CollectionHautdejardin=TVXZROM&HDJ_DAV=R&HDJ_D2=V&HDJ_D1=T&HDJ_D3=X&HDJ_D4=Z&HDJ_SRB=O&CollectionRezdejardin=UWY1SPQM&RDJ_DAV=S&RDJ_D2=W&RDJ_D1=U&RDJ_D3=Y&RDJ_D4=1&RDJ_SRB=P&RDJ_RLR=Q&RICHELIEU_AUTRE=ABCDEEGIKLJ&RCL_D1=A&RCL_D2=K&RCL_D3=D&RCL_D4=E&RCL_D5=E&RCL_D6=C&RCL_D7=B&RCL_D8=J&RCL_D9=G&RCL_D10=I&RCL_D11=L&ARSENAL=H&LivrePeriodique=IP&partitions=C&images_fixes=F&son=S&images_animees=N&Disquette_cederoms=E&multimedia=M&cartes_plans=D&manuscrits=BT&monnaies_medailles_objets=JO&salle_spectacle=V&Monographie_TN=M&Periodique_TN=S&Recueil_TN=R&CollectionEditorial_TN=C&Ensemble_TN=E&Spectacle_TN=A&NoticeB=%s")
|
||
("Gallica Bibliothèque Numérique (web)"
|
||
. "http://gallica.bnf.fr/Search?q=%s"))
|
||
"Alist of online sources that can be used to search for
|
||
publications. The key of each entry is the name of the online
|
||
source. The value is the URL used for retrieving results. This
|
||
URL must contain a %s in the position where the search term
|
||
should be inserted. Alternatively, the value can be a function
|
||
that will be called when the entry is selected."
|
||
:group 'bibtex-completion
|
||
:type '(alist :key-type string
|
||
:value-type (choice (string :tag "URL")
|
||
(function :tag "Function"))))
|
||
|
||
(defcustom bibtex-completion-browser-function nil
|
||
"The browser that is used to access online resources. If
|
||
nil (default), the value of `browse-url-browser-function' is
|
||
used. If that value is nil, Helm uses the first available
|
||
browser in `helm-browse-url-default-browser-alist'"
|
||
:group 'bibtex-completion
|
||
:type '(choice
|
||
(const :tag "Default" :value nil)
|
||
(function-item :tag "Emacs interface to w3m" :value w3m-browse-url)
|
||
(function-item :tag "Emacs W3" :value browse-url-w3)
|
||
(function-item :tag "W3 in another Emacs via `gnudoit'"
|
||
:value browse-url-w3-gnudoit)
|
||
(function-item :tag "Mozilla" :value browse-url-mozilla)
|
||
(function-item :tag "Firefox" :value browse-url-firefox)
|
||
(function-item :tag "Chromium" :value browse-url-chromium)
|
||
(function-item :tag "Galeon" :value browse-url-galeon)
|
||
(function-item :tag "Epiphany" :value browse-url-epiphany)
|
||
(function-item :tag "Netscape" :value browse-url-netscape)
|
||
(function-item :tag "eww" :value eww-browse-url)
|
||
(function-item :tag "Mosaic" :value browse-url-mosaic)
|
||
(function-item :tag "Mosaic using CCI" :value browse-url-cci)
|
||
(function-item :tag "Text browser in an xterm window"
|
||
:value browse-url-text-xterm)
|
||
(function-item :tag "Text browser in an Emacs window"
|
||
:value browse-url-text-emacs)
|
||
(function-item :tag "KDE" :value browse-url-kde)
|
||
(function-item :tag "Elinks" :value browse-url-elinks)
|
||
(function-item :tag "Specified by `Browse Url Generic Program'"
|
||
:value browse-url-generic)
|
||
(function-item :tag "Default Windows browser"
|
||
:value browse-url-default-windows-browser)
|
||
(function-item :tag "Default Mac OS X browser"
|
||
:value browse-url-default-macosx-browser)
|
||
(function-item :tag "GNOME invoking Mozilla"
|
||
:value browse-url-gnome-moz)
|
||
(function-item :tag "Default browser"
|
||
:value browse-url-default-browser)
|
||
(function :tag "Your own function")
|
||
(alist :tag "Regexp/function association list"
|
||
:key-type regexp :value-type function)))
|
||
|
||
(defcustom bibtex-completion-additional-search-fields nil
|
||
"The fields that are used for searching in addition to author,
|
||
editor, title, year, BibTeX key, and entry type."
|
||
:group 'bibtex-completion
|
||
:type '(repeat string))
|
||
|
||
(defcustom bibtex-completion-no-export-fields nil
|
||
"A list of fields that should be ignored when exporting BibTeX
|
||
entries."
|
||
:group 'bibtex-completion
|
||
:type '(repeat string))
|
||
|
||
(defcustom bibtex-completion-cite-commands '("cite" "Cite" "parencite"
|
||
"Parencite" "footcite" "footcitetext" "textcite" "Textcite"
|
||
"smartcite" "Smartcite" "cite*" "parencite*" "supercite" "autocite"
|
||
"Autocite" "autocite*" "Autocite*" "citeauthor" "Citeauthor"
|
||
"citeauthor*" "Citeauthor*" "citetitle" "citetitle*" "citeyear"
|
||
"citeyear*" "citedate" "citedate*" "citeurl" "nocite" "fullcite"
|
||
"footfullcite" "notecite" "Notecite" "pnotecite" "Pnotecite"
|
||
"fnotecite")
|
||
"The list of LaTeX cite commands. When creating LaTeX
|
||
citations, these can be accessed as future entries in the
|
||
minibuffer history, i.e. by pressing the arrow down key. The
|
||
default entries are taken from biblatex. There is currently no
|
||
special support for multicite commands and volcite et al. These
|
||
commands can be used but bibtex-completion does not prompt for their
|
||
extra arguments."
|
||
:group 'bibtex-completion
|
||
:type '(choice string (repeat string)))
|
||
|
||
(defcustom bibtex-completion-cite-default-command "cite"
|
||
"The LaTeX cite command that is used if the user doesn't enter
|
||
anything when prompted for such a command."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-cite-prompt-for-optional-arguments t
|
||
"If t, bibtex-completion prompts for pre- and postnotes for
|
||
LaTeX cite commands. Choose nil for no prompts."
|
||
:group 'bibtex-completion
|
||
:type 'boolean)
|
||
|
||
(defcustom bibtex-completion-cite-default-as-initial-input nil
|
||
"This variable controls how the default command defined in
|
||
`bibtex-completion-cite-default-command' is used. If t, it is inserted
|
||
into the minibuffer before reading input from the user. If nil,
|
||
it is used as the default if the user doesn't enter anything."
|
||
:group 'bibtex-completion
|
||
:type 'boolean)
|
||
|
||
(defcustom bibtex-completion-pdf-field nil
|
||
"The name of the BibTeX field in which the path to PDF files is
|
||
stored or nil if no such field should be used. If an entry has
|
||
no value for this field, or if the specified file does not exist,
|
||
or if this variable is nil, bibtex-completion will look up the PDF in
|
||
the directories listed in `bibtex-completion-library-path'."
|
||
:group 'bibtex-completion
|
||
:type 'string)
|
||
|
||
(defcustom bibtex-completion-display-formats
|
||
'((t . "${author:36} ${title:*} ${year:4} ${=has-pdf=:1}${=has-note=:1} ${=type=:7}"))
|
||
"Alist of format strings for displaying entries in the results list.
|
||
The key of each element of this list is either a BibTeX entry
|
||
type (in which case the format string applies to entries of this
|
||
type only) or t (in which case the format string applies to all
|
||
other entry types). The value is the format string.
|
||
|
||
In the format string, expressions like \"${author:36}\",
|
||
\"${title:*}\", etc, are expanded to the value of the
|
||
corresponding field. An expression like \"${author:N}\" is
|
||
truncated to a width of N characters, whereas an expression like
|
||
\"${title:*}\" is truncated to the remaining width in the results
|
||
window. Three special fields are available: \"=type=\" holds the
|
||
BibTeX entry type, \"=has-pdf=\" holds
|
||
`bibtex-completion-pdf-symbol' if the entry has a PDF file, and
|
||
\"=has-notes=\" holds `bibtex-completion-notes-symbol' if the
|
||
entry has a notes file. The \"author\" field is expanded to
|
||
either the author names or, if the entry has no author field, the
|
||
editor names."
|
||
:group 'bibtex-completion
|
||
:type '(alist :key-type symbol :value-type string))
|
||
|
||
(defvar bibtex-completion-cross-referenced-entry-types
|
||
'("proceedings" "mvproceedings" "book" "mvbook" "collection" "mvcollection")
|
||
"The list of potentially cross-referenced entry types (in
|
||
lowercase). Only entries of these types are checked in
|
||
order to resolve cross-references. The default list is usually
|
||
sufficient; adding more types can slow down resolution for
|
||
large biblioraphies.")
|
||
|
||
(defvar bibtex-completion-display-formats-internal nil
|
||
"Stores `bibtex-completion-display-formats' together with the
|
||
\"used width\" of each format string. This is set internally.")
|
||
|
||
(defvar bibtex-completion-cache nil
|
||
"A cache storing the hash of the bibliography content and the
|
||
corresponding list of entries, for each bibliography file,
|
||
obtained when the bibliography was last parsed. When the
|
||
current bibliography hash is identical to the cached hash, the
|
||
cached list of candidates is reused, otherwise the bibliography
|
||
file is reparsed.")
|
||
|
||
(defvar bibtex-completion-string-cache nil
|
||
"A cache storing bibtex strings, for each bibliography file, obtained when the bibliography was last parsed.")
|
||
|
||
(defvar bibtex-completion-string-hash-table nil
|
||
"A hash table used for string replacements.")
|
||
|
||
|
||
(defun bibtex-completion-normalize-bibliography (&optional type)
|
||
"Returns a list of bibliography file(s) in
|
||
`bibtex-completion-bibliography'. If there are org-mode
|
||
bibliography-files, their corresponding bibtex files are listed
|
||
as well, unless TYPE is 'main. If TYPE is 'bibtex, org-mode
|
||
bibliography-files are instead replaced with their associated
|
||
bibtex files."
|
||
(delete-dups
|
||
(cl-loop
|
||
for bib-file in (-flatten (list bibtex-completion-bibliography))
|
||
for main-file = (if (consp bib-file)
|
||
(car bib-file)
|
||
bib-file)
|
||
for bibtex-file = (if (consp bib-file)
|
||
(cdr bib-file)
|
||
(concat (file-name-sans-extension main-file) ".bib"))
|
||
unless (equal type 'bibtex)
|
||
collect main-file
|
||
unless (equal type 'main)
|
||
collect bibtex-file)))
|
||
|
||
(defvar bibtex-completion-file-watch-descriptors nil
|
||
"List of file watches monitoring bibliography files for changes.")
|
||
|
||
(defun bibtex-completion-init ()
|
||
"Checks that the files and directories specified by the user
|
||
actually exist. Also sets `bibtex-completion-display-formats-internal'."
|
||
|
||
;; Remove current watch-descriptors for bibliography files:
|
||
(mapc (lambda (watch-descriptor)
|
||
(file-notify-rm-watch watch-descriptor))
|
||
bibtex-completion-file-watch-descriptors)
|
||
(setq bibtex-completion-file-watch-descriptors nil)
|
||
|
||
;; Check that all specified bibliography files exist and add file
|
||
;; watches for automatic reloading of the bibliography when a file
|
||
;; is changed:
|
||
(mapc (lambda (file)
|
||
(if (f-file? file)
|
||
(let ((watch-descriptor
|
||
(file-notify-add-watch file
|
||
'(change)
|
||
(lambda (event) (bibtex-completion-candidates)))))
|
||
(setq bibtex-completion-file-watch-descriptors
|
||
(cons watch-descriptor bibtex-completion-file-watch-descriptors)))
|
||
(user-error "Bibliography file %s could not be found." file)))
|
||
(bibtex-completion-normalize-bibliography))
|
||
|
||
;; Pre-calculate minimal widths needed by the format strings for
|
||
;; various entry types:
|
||
(setq bibtex-completion-display-formats-internal
|
||
(mapcar (lambda (format)
|
||
(let* ((format-string (cdr format))
|
||
(fields-width 0)
|
||
(string-width
|
||
(length
|
||
(s-format format-string
|
||
(lambda (field)
|
||
(setq fields-width
|
||
(+ fields-width
|
||
(string-to-number
|
||
(or (cadr (split-string field ":"))
|
||
""))))
|
||
"")))))
|
||
(-cons* (car format) format-string (+ fields-width string-width))))
|
||
bibtex-completion-display-formats)))
|
||
|
||
(defun bibtex-completion-clear-cache (&optional files)
|
||
"Clears FILES from cache. If FILES is omitted, all files in `bibtex-completion-biblography' are cleared."
|
||
(setq bibtex-completion-cache
|
||
(cl-remove-if
|
||
(lambda (x)
|
||
(member (car x)
|
||
(or files
|
||
(bibtex-completion-normalize-bibliography 'bibtex))))
|
||
bibtex-completion-cache)))
|
||
|
||
(defun bibtex-completion-clear-string-cache (&optional files)
|
||
"Clears FILES from cache. If FILES is omitted, all files in `bibtex-completion-bibliography' are cleared."
|
||
(setq bibtex-completion-string-cache
|
||
(cl-remove-if
|
||
(lambda (x)
|
||
(member (car x)
|
||
(or files
|
||
(-flatten (list bibtex-completion-bibliography)))))
|
||
bibtex-completion-string-cache)))
|
||
|
||
(defun bibtex-completion-parse-strings (&optional ht-strings)
|
||
"Parse the BibTeX strings listed in the current buffer and
|
||
return a list of entries in the order in which they appeared in
|
||
the BibTeX file.
|
||
|
||
If HT-STRINGS is provided it is assumed to be a hash table used
|
||
for string replacement."
|
||
(goto-char (point-min))
|
||
(let ((strings (cl-loop
|
||
with ht = (if ht-strings ht-strings (make-hash-table :test #'equal))
|
||
for entry-type = (parsebib-find-next-item)
|
||
while entry-type
|
||
if (string= (downcase entry-type) "string")
|
||
collect (let ((entry (parsebib-read-string (point) ht)))
|
||
(puthash (car entry) (cdr entry) ht)
|
||
entry)
|
||
)))
|
||
(-filter (lambda (x) x) strings)))
|
||
|
||
(defun bibtex-completion-update-strings-ht (ht strings)
|
||
(cl-loop
|
||
for entry in strings
|
||
do (puthash (car entry) (cdr entry) ht)))
|
||
|
||
(defvar bibtex-completion-cached-notes-keys nil
|
||
"A cache storing notes keys obtained when the bibliography was last parsed.")
|
||
|
||
(defun bibtex-completion-candidates ()
|
||
"Reads the BibTeX files and returns a list of conses, one for
|
||
each entry. The first element of these conses is a string
|
||
containing authors, editors, title, year, type, and key of the
|
||
entry. This is string is used for matching. The second element
|
||
is the entry (only the fields listed above) as an alist."
|
||
(let ((files (nreverse (bibtex-completion-normalize-bibliography 'bibtex)))
|
||
(ht-strings (make-hash-table :test #'equal))
|
||
reparsed-files)
|
||
|
||
;; Open each bibliography file in a temporary buffer,
|
||
;; check hash of bibliography and mark for reparsing if necessary:
|
||
|
||
(cl-loop
|
||
for file in files
|
||
do
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(let ((bibliography-hash (secure-hash 'sha256 (current-buffer))))
|
||
(unless (string= (cadr (assoc file bibtex-completion-cache))
|
||
bibliography-hash)
|
||
;; Mark file as reparsed.
|
||
;; This will be useful to resolve cross-references:
|
||
(push file reparsed-files)))))
|
||
|
||
(when (and bibtex-completion-notes-path
|
||
(f-file? bibtex-completion-notes-path))
|
||
(with-temp-buffer
|
||
(org-mode) ;; need this to avoid error in emacs 25.3.1
|
||
(insert-file-contents bibtex-completion-notes-path)
|
||
(setq bibtex-completion-cached-notes-keys
|
||
(let ((tree (org-element-parse-buffer 'headline)))
|
||
(org-element-map tree 'headline
|
||
(lambda (key) (org-element-property :CUSTOM_ID key)))))))
|
||
|
||
;; reparse if necessary
|
||
|
||
(when reparsed-files
|
||
(cl-loop
|
||
for file in files
|
||
do
|
||
(with-temp-buffer
|
||
(insert-file-contents file)
|
||
(let ((bibliography-hash (secure-hash 'sha256 (current-buffer))))
|
||
(if (not (member file reparsed-files))
|
||
(bibtex-completion-update-strings-ht ht-strings
|
||
(cddr (assoc file bibtex-completion-string-cache)))
|
||
(progn
|
||
(message "Parsing bibliography file %s ..." file)
|
||
(bibtex-completion-clear-string-cache (list file))
|
||
(push (-cons* file
|
||
bibliography-hash
|
||
(bibtex-completion-parse-strings ht-strings))
|
||
bibtex-completion-string-cache)
|
||
|
||
(bibtex-completion-clear-cache (list file))
|
||
(push (-cons* file
|
||
bibliography-hash
|
||
(bibtex-completion-parse-bibliography ht-strings))
|
||
bibtex-completion-cache))))))
|
||
(setf bibtex-completion-string-hash-table ht-strings))
|
||
|
||
;; If some files were reparsed, resolve cross-references:
|
||
(when reparsed-files
|
||
(message "Resolving cross-references ...")
|
||
(bibtex-completion-resolve-crossrefs files reparsed-files))
|
||
|
||
;; Finally return the list of candidates:
|
||
(message "Done (re)loading bibliography.")
|
||
(nreverse
|
||
(cl-loop
|
||
for file in files
|
||
append (cddr (assoc file bibtex-completion-cache))))))
|
||
|
||
(defun bibtex-completion-resolve-crossrefs (files reparsed-files)
|
||
"Expand all entries with fields from cross-referenced entries
|
||
in FILES, assuming that only those files in REPARSED-FILES were
|
||
reparsed whereas the other files in FILES were up-to-date."
|
||
(cl-loop
|
||
with entry-hash = (bibtex-completion-make-entry-hash files reparsed-files)
|
||
for file in files
|
||
for entries = (cddr (assoc file bibtex-completion-cache))
|
||
if (member file reparsed-files)
|
||
;; The file was reparsed.
|
||
;; Resolve crossrefs then make candidates for all entries:
|
||
do (setf
|
||
(cddr (assoc file bibtex-completion-cache))
|
||
(cl-loop
|
||
for entry in entries
|
||
;; Entries are alists of \(FIELD . VALUE\) pairs.
|
||
for crossref = (bibtex-completion-get-value "crossref" entry)
|
||
collect (bibtex-completion-make-candidate
|
||
(if crossref
|
||
(bibtex-completion-remove-duplicated-fields
|
||
;; Insert an empty field so we can discard the crossref info if needed:
|
||
(append entry
|
||
(cl-acons "" ""
|
||
(gethash (downcase crossref) entry-hash))))
|
||
entry))))
|
||
else
|
||
;; The file was not reparsed.
|
||
;; Resolve crossrefs then make candidates for the entries with a crossref field:
|
||
do (setf
|
||
(cddr (assoc file bibtex-completion-cache))
|
||
(cl-loop
|
||
for entry in entries
|
||
;; Entries are \(STRING . ALIST\) conses.
|
||
for entry-alist = (cdr entry)
|
||
for crossref = (bibtex-completion-get-value "crossref" entry-alist)
|
||
collect (if crossref
|
||
(bibtex-completion-make-candidate
|
||
(bibtex-completion-remove-duplicated-fields
|
||
;; Discard crossref info and resolve crossref again:
|
||
(append (--take-while (> (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)
|
||
(with-temp-buffer
|
||
(mapc #'insert-file-contents
|
||
(bibtex-completion-normalize-bibliography 'bibtex))
|
||
(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: ")
|
||
"")))
|
||
(if (and (string= "" prenote) (string= "" postnote))
|
||
(format "\\%s{%s}" cite-command (s-join ", " keys))
|
||
(format "\\%s[%s][%s]{%s}" cite-command prenote postnote (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))))
|
||
|
||
(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:
|
||
(if (string= field "author-or-editor")
|
||
(if-let* ((value (bibtex-completion-get-value "author" entry)))
|
||
(bibtex-completion-apa-format-authors value)
|
||
(bibtex-completion-apa-format-editors
|
||
(bibtex-completion-get-value "editor" entry)))
|
||
;; Real fields:
|
||
(if-let* ((value (bibtex-completion-get-value field entry)))
|
||
(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 ; 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-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 (file-name-nondirectory 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))
|
||
(insert (s-format bibtex-completion-notes-template-one-file
|
||
'bibtex-completion-apa-get-value
|
||
entry)))
|
||
(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 or a file."
|
||
(let* ((key (car keys))
|
||
(source (char-to-string
|
||
(read-char-choice "Add pdf from [b]uffer or [f]ile? " '(?b ?f))))
|
||
(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))))
|
||
(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)))))
|
||
|
||
(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))
|
||
(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
|