From 88f7d95d7fea905231e2c61dea50bec28320e266 Mon Sep 17 00:00:00 2001 From: Roshan Shariff Date: Sun, 17 Jul 2022 02:29:33 -0600 Subject: [PATCH] API: resource getter functions now return hash tables. The `citar-get-files`, `citar-get-links`, and `citar-get-notes` functions now return hash tables mapping each key to a list of associated resources. They used to just return lists of resources for all the given keys, but that interface doesn't allow callers to determine which key corresponds to each resource. The `:items` function in the notes source API has also been modified, and must now return a hash table instead of a list of notes. --- citar-file.el | 59 ++++++------- citar.el | 233 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 178 insertions(+), 114 deletions(-) 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