diff --git a/citar-file.el b/citar-file.el index 0e4c3b47..58283b69 100644 --- a/citar-file.el +++ b/citar-file.el @@ -26,10 +26,10 @@ (make-obsolete-variable 'citar-file-extensions 'citar-library-file-extensions "1.0") -(declare-function citar-get-value "citar") -(declare-function citar--bibliography-files "citar") -(declare-function citar--check-configuration "citar") -(declare-function citar--get-notes-config "citar") +(declare-function citar-get-value "citar" (field key-or-entry)) +(declare-function citar--bibliography-files "citar" (&rest buffers)) +(declare-function citar--check-configuration "citar" (&rest variables)) +(declare-function citar--get-resources-using-function "citar" (func &optional keys)) ;;;; File related variables @@ -163,13 +163,15 @@ Note: this function is intended to be used in `citar-has-files-functions'. Use `citar-has-files' to test whether entries have associated files." (when citar-file-variable - (apply-partially #'citar-get-value citar-file-variable))) + (lambda (citekey) (and (citar-get-value citar-file-variable citekey) t)))) -(defun citar-file--get-from-file-field (keys) - "Return list of files for KEYS. +(defun citar-file--get-from-file-field (&optional keys) + "Return files for KEYS by parsing the `citar-file-variable' field. -Parse and return files given in the bibliography field named by -`citar-file-variable'. +Return a hash table mapping each element of KEYS to a list of +files given in the bibliography entry named by +`citar-file-variable'. If KEYS is nil, return files for all +entries. Note: this function is intended to be used in `citar-get-files-functions'. Use `citar-get-files' to get all @@ -179,9 +181,9 @@ files associated with KEYS." 'citar-file-parser-functions) (let ((dirs (append citar-library-paths (mapcar #'file-name-directory (citar--bibliography-files))))) - (mapcan - (lambda (citekey) - (when-let ((fieldvalue (citar-get-value filefield citekey))) + (citar--get-resources-using-function + (lambda (citekey entry) + (when-let ((fieldvalue (citar-get-value filefield entry))) (citar-file--parse-file-field fieldvalue dirs citekey))) keys)))) @@ -189,20 +191,17 @@ files associated with KEYS." (defun citar-file--has-library-files () "Return predicate testing whether cite key has library files." - (citar--check-configuration 'citar-library-paths 'citar-library-file-extensions) - (let ((files (citar-file--directory-files - citar-library-paths nil citar-library-file-extensions - citar-file-additional-files-separator))) - (lambda (key) - (gethash key files)))) + (let ((files (citar-file--get-library-files))) + (unless (hash-table-empty-p files) + (lambda (citekey) + (and (gethash citekey files) t))))) -(defun citar-file--get-library-files (keys) +(defun citar-file--get-library-files (&optional keys) "Return list of files for KEYS in ENTRIES." - (citar--check-configuration 'citar-library-paths) - (let ((files (citar-file--directory-files citar-library-paths keys - citar-library-file-extensions - citar-file-additional-files-separator))) - (mapcan (lambda (key) (gethash key files)) keys))) + (citar--check-configuration 'citar-library-paths 'citar-library-file-extensions) + (citar-file--directory-files + citar-library-paths keys citar-library-file-extensions + citar-file-additional-files-separator)) (defun citar-file--make-filename-regexp (keys extensions &optional additional-sep) "Regexp matching file names starting with KEYS and ending with EXTENSIONS. @@ -307,7 +306,7 @@ need to scan the contents of DIRS in this case." ;;;; Note files -(defun citar-file--note-directory-files (&optional keys) +(defun citar-file--get-notes (&optional keys) "Return note files associated with KEYS. Return hash table whose keys are elements of KEYS and values are lists of note file names found in `citar-notes-paths' having @@ -319,8 +318,9 @@ extensions in `citar-file-note-extensions'." (defun citar-file--has-notes () "Return predicate testing whether cite key has associated notes." - (let ((files (citar-file--note-directory-files))) - (lambda (key) (gethash key files)))) + (let ((notes (citar-file--get-notes))) + (unless (hash-table-empty-p notes) + (lambda (citekey) (and (gethash citekey notes) t))))) (defun citar-file--create-note (key entry) "Create a note file from KEY and ENTRY." @@ -331,11 +331,6 @@ extensions in `citar-file-note-extensions'." (funcall citar-note-format-function key entry))) (user-error "Make sure `citar-notes-paths' and `citar-file-note-extensions' are non-nil"))) -(defun citar-file--get-notes (keys) - "Return list of notes associated with KEYS." - (let ((notes (citar-file--note-directory-files keys))) - (apply #'append (map-values notes)))) - (defun citar-file--get-note-filename (key) "Return existing or new note filename for KEY. diff --git a/citar.el b/citar.el index 5115f47f..c9708edf 100644 --- a/citar.el +++ b/citar.el @@ -147,12 +147,13 @@ by this variable." (string :tag "Field name") (const :tag "Ignore cross-references" nil))) -(defcustom citar-additional-fields '("doi" "url" "pmcid" "pmid") +(defcustom citar-additional-fields nil "A list of fields to add to parsed data. By default, citar filters parsed data based on the fields -specified in `citar-templates'. This specifies additional fields -to include." +specified in `citar-templates', `citar-file-variable' +`citar-crossref-variable', and `citar-link-fields'. This +specifies additional fields to include." :group 'citar :type '(repeat string)) @@ -261,17 +262,21 @@ If nil, single resources will open without prompting." ;;;; File, note, and URL handling -(defcustom citar-has-files-functions (list #'citar-file--has-file-field - #'citar-file--has-library-files) - "List of functions to test if an entry has associated files." - :group 'citar - :type '(repeat function)) +(defcustom citar-file-sources (list (list :items #'citar-file--get-from-file-field + :hasitems #'citar-file--has-file-field) + (list :items #'citar-file--get-library-files + :hasitems #'citar-file--has-library-files)) + "List of backends used to get library files for bibliography references. + +Should be a list of plists, where each plist has the following properties: + + :items Function that takes a list of citation keys and returns + a hash table mapping each of those keys to a list of files. -(defcustom citar-get-files-functions (list #'citar-file--get-from-file-field - #'citar-file--get-library-files) - "List of functions to find files associated with entries." + :hasitems Function that takes a citation key and returns + non-nil if it has associated files." :group 'citar - :type '(repeat function)) + :type '(repeat (plist :value-type function :options (:items :hasitems)))) (defcustom citar-notes-sources `((citar-file . @@ -314,6 +319,19 @@ plist has the following properties: :group 'citar :type 'function) +(defcustom citar-link-fields '((doi . "https://doi.org/%s") + (pmid . "https://www.ncbi.nlm.nih.gov/pubmed/%s") + (pmcid . "https://www.ncbi.nlm.nih.gov/pmc/articles/%s") + (url . "%s")) + "Bibliography fields to parse into links. + +Association list whose keys are symbols naming bibliography +fields and values are URL strings. In each URL, \"%s\" is +replaced by the contents of the corresponding field." + :group 'citar + :type '(alist :key-type symbol :value-type string)) + + ;;;; Major mode functions ;; TODO Move this to `citar-org', since it's only used there? @@ -613,11 +631,12 @@ returning only links, or the category specified by resources of multiple types, CATEGORY is `multi-category' and the `multi-category' text property is applied to each element of CANDIDATES." - (cl-flet ((withtype (type cands) (mapcar (lambda (cand) (propertize cand 'citar--resource type)) cands))) + (cl-flet ((withtype (type cands) (mapcar (lambda (cand) (propertize cand 'citar--resource type)) cands)) + (getresources (table) (when table (delete-dups (apply #'append (hash-table-values table)))))) (let* ((citar--entries (citar-get-entries)) - (files (if (listp files) files (citar-get-files key-or-keys))) - (links (if (listp links) links (citar-get-links key-or-keys))) - (notes (if (listp notes) notes (citar-get-notes key-or-keys))) + (files (if (listp files) files (getresources (citar-get-files key-or-keys)))) + (links (if (listp links) links (getresources (citar-get-links key-or-keys)))) + (notes (if (listp notes) notes (getresources (citar-get-notes key-or-keys)))) (notecat (citar--get-notes-config :category)) (sources (nconc (when files (list (cons 'file (withtype 'file files)))) (when links (list (cons 'url (withtype 'url links)))) @@ -849,12 +868,18 @@ NAME is a symbol, and CONFIG is a plist." (cl-callf2 assq-delete-all name citar-notes-sources)) (cl-defun citar-get-notes (&optional (key-or-keys nil filter-p)) - "Return list of notes associated with KEY-OR-KEYS. -If KEY-OR-KEYS is omitted, return all notes." - (let* ((citar--entries (citar-get-entries)) - (keys (citar--with-crossref-keys key-or-keys))) - (unless (and filter-p (null keys)) ; return nil if KEY-OR-KEYS was given, but is nil - (delete-dups (funcall (citar--get-notes-config :items) keys))))) + "Return notes associated with KEY-OR-KEYS. + +KEY-OR-KEYS should be either a list KEYS or a single key. Return +a hash table mapping elements of KEYS to lists of associated +notes found using `citar-notes-source'. Include notes associated +with cross-referenced keys. + +If KEY-OR-KEYS is omitted, return notes for all entries. If it is +nil, return nil." + (when (or key-or-keys (not filter-p)) + (citar--get-resources key-or-keys + (citar--get-notes-config :items)))) (defun citar-create-note (key &optional entry) "Create a note for KEY and ENTRY. @@ -862,34 +887,43 @@ If ENTRY is nil, use `citar-get-entry' with KEY." (interactive (list (citar-select-ref))) (funcall (citar--get-notes-config :create) key (or entry (citar-get-entry key)))) -(defun citar-get-files (key-or-keys) - "Return list of files associated with KEY-OR-KEYS. -Find files using `citar-get-files-functions'. Include files -associated with cross-referenced keys." - (let ((citar--entries (citar-get-entries))) - (when-let ((keys (citar--with-crossref-keys key-or-keys))) - (delete-dups (mapcan (lambda (func) (funcall func keys)) citar-get-files-functions))))) - - -(defun citar-get-links (key-or-keys) - "Return list of links associated with KEY-OR-KEYS. -Include files associated with cross-referenced keys." - (let* ((citar--entries (citar-get-entries)) - (keys (citar--with-crossref-keys key-or-keys))) - (delete-dups - (mapcan - (lambda (key) - (when-let ((entry (citar-get-entry key))) - (mapcan - (pcase-lambda (`(,fieldname . ,urlformat)) - (when-let ((fieldvalue (citar-get-value fieldname entry))) - (list (format urlformat fieldvalue)))) - '((doi . "https://doi.org/%s") - (pmid . "https://www.ncbi.nlm.nih.gov/pubmed/%s") - (pmcid . "https://www.ncbi.nlm.nih.gov/pmc/articles/%s") - (url . "%s"))))) - keys)))) - +(cl-defun citar-get-files (&optional (key-or-keys nil filter-p)) + "Return files associated with KEY-OR-KEYS. + +KEY-OR-KEYS should be either a list KEYS or a single key. Return +a hash table mapping elements of KEYS to lists of associated +files found using `citar-file-sources'. Include files associated +with cross-referenced keys. + +If KEY-OR-KEYS is omitted, return files for all entries. If it is +nil, return nil." + (when (or key-or-keys (not filter-p)) + (citar--get-resources key-or-keys + (mapcar (lambda (source) + (plist-get source :items)) + citar-file-sources)))) + +(cl-defun citar-get-links (&optional (key-or-keys nil filter-p)) + "Return links associated with KEY-OR-KEYS. + +KEY-OR-KEYS should be either a list KEYS or a single key. Return +a hash table mapping elements of KEYS to lists of associated +links found using `citar-link-fields'. Include links associated +with cross-referenced keys. + +If KEY-OR-KEYS is omitted, return notes for all entries. If it is +nil, return nil." + (when (or key-or-keys (not filter-p)) + (citar--get-resources key-or-keys + (apply-partially + #'citar--get-resources-using-function + (lambda (_citekey entry) + (let (keylinks) + (when entry + (pcase-dolist (`(,fieldname . ,urlformat) citar-link-fields) + (when-let ((fieldvalue (citar-get-value fieldname entry))) + (push (format urlformat fieldvalue) keylinks)))) + (nreverse keylinks))))))) (defun citar-has-files () "Return predicate testing whether entry has associated files. @@ -907,11 +941,14 @@ For example, to test whether KEY has associated files: When testing many keys, call this function once and use the returned predicate repeatedly. -Files are detected using `citar-has-files-functions', which see. -Also check any bibliography entries that are cross-referenced -from the given KEY; see `citar-crossref-variable'." +Files are detected using `citar-file-sources', which see. Also +check any bibliography entries that are cross-referenced from the +given KEY; see `citar-crossref-variable'." (citar--has-resources - (mapcar #'funcall citar-has-files-functions))) + (mapcar (lambda (source) + (when-let ((hasitems (plist-get source :hasitems))) + (funcall hasitems))) + citar-file-sources))) (defun citar-has-notes () @@ -944,8 +981,9 @@ Return a function that takes KEY and returns non-nil when the corresponding bibliography entry has associated links. See the documentation of `citar-has-files' and `citar-has-notes', which have similar usage." - (citar--has-resources - (apply-partially #'citar-get-field-with-value '(doi pmid pmcid url)))) + (let ((linkfields (mapcar (lambda (field) (symbol-name (car field))) citar-link-fields))) + (citar--has-resources + (apply-partially #'citar-get-field-with-value linkfields)))) (defun citar--has-resources (predicates) @@ -981,6 +1019,55 @@ another entry in ENTRIES that has associated resources." (funcall hasresourcep xkey)))) hasresourcep))) +(defun citar--get-resources (key-or-keys functions) + "Return hash table mapping each element of KEY-OR-KEYS to a list of resources. + +KEY-OR-KEYS should be either a list KEYS or a single key. +FUNCTIONS should be a list of functions, each of which takes a +list of bibliography keys and returns a hash table mapping each +of those keys to a list of resources. FUNCTIONS may also be a +single such function. + +Return a hash table mapping each element of KEYS to the +concatenated list of resources returned by all the FUNCTIONS. +Also include resources associated with cross-references from +KEYS." + (let* ((citar--entries (citar-get-entries)) + (keys (if (listp key-or-keys) (delete-dups key-or-keys) (list key-or-keys))) + (functions (if (functionp functions) (list functions) (remq nil functions))) + (xref citar-crossref-variable) + (getxref (apply-partially #'citar-get-value xref)) + (xkeys (if (not xref) + keys + (delete-dups (append keys (delq nil (mapcar getxref keys)))))) + (resources (delq nil (mapcar (lambda (func) (funcall func xkeys)) functions)))) + (cl-flet* ((getreslists (citekey) (delq nil (mapcar (apply-partially #'gethash citekey) resources))) + (xresources (citekey entry) (apply #'append + (nconc (getreslists citekey) + (when-let ((xkey (and xref + (citar-get-value xref entry)))) + (getreslists xkey)))))) + (citar--get-resources-using-function #'xresources keys)))) + +(defun citar--get-resources-using-function (func &optional keys) + "Collect resources for KEYS returned by FUNC. + +Return a hash table mapping each element of KEYS to the result of +calling FUNC on that key and corresponding bibliography entry. If +KEYS is nil, call FUNC on every key and entry returned by +`citar-get-entries'. + +Note: This is a helper function to make it easier to write +getters for file, note, and link resources." + (let ((resources (make-hash-table :test 'equal))) + (prog1 resources + (cl-flet ((putresult (citekey entry) (when-let ((result (funcall func citekey entry))) + (puthash citekey result resources)))) + (if (null keys) + (maphash #'putresult (citar-get-entries)) + (dolist (citekey keys) + (putresult citekey (citar-get-entry citekey)))))))) + ;;; Format and display field values ;; Lifted from bibtex-completion @@ -1022,27 +1109,9 @@ personal names of the form \"family, given\"." (list citar-file-variable)) ,@(when citar-crossref-variable (list citar-crossref-variable)) + ,@(mapcar (lambda (field) (symbol-name (car field))) citar-link-fields) . ,citar-additional-fields))) -(defun citar--with-crossref-keys (key-or-keys) - "Return KEY-OR-KEYS augmented with cross-referenced items. - -KEY-OR-KEYS is either a list KEYS or a single key, which is -converted into KEYS. Return a list containing the elements of -KEYS, with each element followed by the corresponding -cross-referenced keys, if any. - -Duplicate keys are removed from the returned list." - (let ((xref citar-crossref-variable) - (keys (if (listp key-or-keys) key-or-keys (list key-or-keys)))) - (delete-dups - (if (not xref) - keys - (mapcan (lambda (key) - (cons key (when-let ((xkey (citar-get-value xref key))) - (list xkey)))) - keys))))) - ;;; Affixations and annotations (defun citar--ref-affix (cands) @@ -1165,10 +1234,9 @@ select a single file." (funcall action (cdr resource)) (error "Expected resource of type `file', got `%s': %S" (car resource) (cdr resource))) (ignore - ;; If some key had files according to `citar-has-files', but `citar-get-files' returned nothing, then - ;; don't print the following message. The appropriate function in `citar-get-files-functions' is - ;; responsible for telling the user why it failed, and we want that explanation to appear in the echo - ;; area. + ;; If some key had files according to the `:hasitems' function, but `:items' returned nothing, then + ;; don't print the following message. The `:items' function is responsible for telling the user why it + ;; failed, and we want that explanation to appear in the echo area. (let ((keys (if (listp key-or-keys) key-or-keys (list key-or-keys))) (hasfilep (citar-has-files))) (unless (and hasfilep (seq-some hasfilep keys)) @@ -1178,9 +1246,10 @@ select a single file." (defun citar-open-notes (keys) "Open notes associated with the KEYS." (interactive (list (citar-select-refs))) - (if-let ((notes (citar-get-notes keys))) - (progn (mapc (citar--get-notes-config :open) notes) - (let ((count (length notes))) + (if-let* ((notes (citar-get-notes keys)) + (noteslist (delete-dups (apply #'append (hash-table-values notes))))) + (progn (mapc (citar--get-notes-config :open) noteslist) + (let ((count (length noteslist))) (when (> count 1) (message "Opened %d notes" count)))) (when keys