diff --git a/Makefile b/Makefile
index 168ac0c8..89a3d84d 100644
--- a/Makefile
+++ b/Makefile
@@ -7,3 +7,7 @@ clean:
lint:
eldev -C --unstable -T lint
+.PHONY: test
+test:
+ eldev -C --unstable -T test
+
diff --git a/citar-cache.el b/citar-cache.el
index 78dc4dab..73453c1f 100644
--- a/citar-cache.el
+++ b/citar-cache.el
@@ -1,22 +1,8 @@
;;; citar-cache.el --- Cache functions for citar -*- lexical-binding: t; -*-
;;
-;; Copyright (C) 2022 Bruce D'Arcus, Roshan Shariff
-;;
-;; This file is not part of GNU Emacs.
-;;
-;; 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 .
-;;
+;; SPDX-FileCopyrightText: 2022 Bruce D'Arcus, Roshan Shariff
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
;;; Commentary:
;;
;; Functions for caching bibliography files.
diff --git a/citar-capf.el b/citar-capf.el
index fc5f5bfb..2a098ae1 100644
--- a/citar-capf.el
+++ b/citar-capf.el
@@ -1,11 +1,10 @@
;;; citar-capf.el --- citar completion-at-point -*- lexical-binding: t; -*-
;;
-;; Copyright (C) 2022 Bruce D'Arcus, Colin McLear
+;; SPDX-FileCopyrightText: 2022 Bruce D'Arcus, Colin McLear
+;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;; This file is not part of GNU Emacs.
;;
-;; SPDX-License-Identifier: GPL-3.0-or-later
-;;
;;; Commentary:
;;
;; Citation key 'completion-at-point' for org, markdown, or latex.
diff --git a/citar-citeproc.el b/citar-citeproc.el
index 02d96bbb..9537b6e7 100644
--- a/citar-citeproc.el
+++ b/citar-citeproc.el
@@ -1,19 +1,7 @@
;;; citar-citeproc.el --- Citeproc reference support for citar -*- lexical-binding: t; -*-
;;
-;; This file is not part of GNU Emacs.
-;;
-;; 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 .
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus
+;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
diff --git a/citar-embark.el b/citar-embark.el
index f3cee0b5..ff6fb9d7 100644
--- a/citar-embark.el
+++ b/citar-embark.el
@@ -1,12 +1,13 @@
;;; citar-embark.el --- Citar/Embark integration -*- lexical-binding: t; -*-
;;
-;; Copyright (C) 2022 Bruce D'Arcus
-;;
;; Author: Bruce D'Arcus
;; Maintainer: Bruce D'Arcus
;; Created: June 22, 2022
;; Modified: June 22, 2022
+;;
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus
;; SPDX-License-Identifier: GPL-3.0-or-later
+;;
;; Version: 1.0
;; Keywords: bib extensions
;; Homepage: https://github.com/emacs-citar/citar-embark
@@ -52,7 +53,8 @@
(defvar citar-embark--multitarget-actions
(list #'citar-open #'citar-open-files #'citar-attach-files #'citar-open-links
#'citar-insert-bibtex #'citar-insert-citation #'citar-insert-reference
- #'citar-copy-reference #'citar-insert-keys #'citar-run-default-action))
+ #'citar-copy-reference #'citar-insert-keys #'citar-run-default-action
+ #'citar-open-notes))
(defvar citar-embark--target-injection-hooks
(list (list #'citar-insert-edit #'embark--ignore-target)))
diff --git a/citar-file.el b/citar-file.el
index 7e68759b..0e4c3b47 100644
--- a/citar-file.el
+++ b/citar-file.el
@@ -1,22 +1,10 @@
;;; citar-file.el --- File functions for citar -*- lexical-binding: t; -*-
;;
-;; Copyright (C) 2021 Bruce D'Arcus
-;;
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus
+;; SPDX-License-Identifier: GPL-3.0-or-later
+
;; This file is not part of GNU Emacs.
;;
-;; This program is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; This program is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-;; GNU General Public License for more details.
-
-;; You should have received a copy of the GNU General Public License
-;; along with this program. If not, see .
-;;
;;; Commentary:
;;
;; Functions for opening and creating bibliographic files related to a source.
@@ -95,9 +83,9 @@ separator that does not otherwise occur in citation keys."
(regexp :tag "Filename separator")))
(defvar citar-notes-paths)
-(defvar citar-create-note-function)
(defvar citar-library-paths)
(defvar citar-library-file-extensions)
+(defvar citar-note-format-function)
;;;; Convenience functions for files and paths
@@ -140,49 +128,45 @@ Example: ':/path/to/test.pdf:PDF'."
(push escaped filenames))))))
(nreverse filenames)))
-(defun citar-file--parse-file-field (entry dirs &optional citekey)
- "Return files found in file field of ENTRY.
+(defun citar-file--parse-file-field (fieldvalue dirs &optional citekey)
+ "Return files found in file field FIELDVALUE.
Relative file names are expanded from the first directory in DIRS
in which they are found. Omit non-existing absolute file names
and relative file names not found in DIRS. On failure, print a
message explaining the cause; CITEKEY is included in this failure
message."
- (when-let* ((fieldname citar-file-variable)
- (fieldvalue (citar-get-value fieldname entry)))
- (if-let ((files (delete-dups (mapcan (lambda (parser)
- (funcall parser fieldvalue))
- citar-file-parser-functions))))
- (if-let ((foundfiles (citar-file--find-files-in-dirs files dirs)))
- (if (null citar-library-file-extensions)
- foundfiles
- (or (seq-filter (lambda (file)
- (member (file-name-extension file) citar-library-file-extensions))
- foundfiles)
- (ignore
- (message "No files for `%s' with `citar-library-file-extensions': %S"
- citekey foundfiles))))
- (ignore
- (message (concat "None of the files for `%s' exist; check `citar-library-paths' and "
- "`citar-file-parser-functions': %S")
- citekey files)))
- (ignore
- (if (string-empty-p (string-trim fieldvalue))
- (message "Empty `%s' field: %s" fieldname citekey)
- (message "Could not parse `%s' field of `%s'; check `citar-file-parser-functions': %s"
- fieldname citekey fieldvalue))))))
-
-(defun citar-file--has-file-field (entries)
+ (if-let ((files (delete-dups (mapcan (lambda (parser)
+ (funcall parser fieldvalue))
+ citar-file-parser-functions))))
+ (if-let ((foundfiles (citar-file--find-files-in-dirs files dirs)))
+ (if (null citar-library-file-extensions)
+ foundfiles
+ (or (seq-filter (lambda (file)
+ (member (file-name-extension file) citar-library-file-extensions))
+ foundfiles)
+ (ignore
+ (message "No files for `%s' with `citar-library-file-extensions': %S"
+ citekey foundfiles))))
+ (ignore
+ (message (concat "None of the files for `%s' exist; check `citar-library-paths' and "
+ "`citar-file-parser-functions': %S")
+ citekey files)))
+ (ignore
+ (if (string-empty-p (string-trim fieldvalue))
+ (message "Empty `%s' field: %s" citar-file-variable citekey)
+ (message "Could not parse `%s' field of `%s'; check `citar-file-parser-functions': %s"
+ citar-file-variable citekey fieldvalue)))))
+
+(defun citar-file--has-file-field ()
"Return predicate to test if bibliography entry in ENTRIES has a file field.
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-let ((filefield citar-file-variable))
- (lambda (key)
- (when-let ((entry (gethash key entries)))
- (citar-get-value filefield entry)))))
+ (when citar-file-variable
+ (apply-partially #'citar-get-value citar-file-variable)))
-(defun citar-file--get-from-file-field (keys entries)
- "Return list of FILES for KEYS given in ENTRIES.
+(defun citar-file--get-from-file-field (keys)
+ "Return list of files for KEYS.
Parse and return files given in the bibliography field named by
`citar-file-variable'.
@@ -190,20 +174,20 @@ Parse and return files given in the bibliography field named by
Note: this function is intended to be used in
`citar-get-files-functions'. Use `citar-get-files' to get all
files associated with KEYS."
- (when citar-file-variable
+ (when-let ((filefield citar-file-variable))
(citar--check-configuration 'citar-library-paths 'citar-library-file-extensions
'citar-file-parser-functions)
(let ((dirs (append citar-library-paths
(mapcar #'file-name-directory (citar--bibliography-files)))))
(mapcan
(lambda (citekey)
- (when-let ((entry (gethash citekey entries)))
- (citar-file--parse-file-field entry dirs citekey)))
+ (when-let ((fieldvalue (citar-get-value filefield citekey)))
+ (citar-file--parse-file-field fieldvalue dirs citekey)))
keys))))
;;;; Scanning library directories
-(defun citar-file--has-library-files (&optional _entries)
+(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
@@ -212,7 +196,7 @@ files associated with KEYS."
(lambda (key)
(gethash key files))))
-(defun citar-file--get-library-files (keys &optional _entries)
+(defun citar-file--get-library-files (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
@@ -323,51 +307,50 @@ need to scan the contents of DIRS in this case."
;;;; Note files
-(defun citar-file--get-notes-hash (&optional keys)
- "Return hash-table with KEYS with file notes."
+(defun citar-file--note-directory-files (&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
+extensions in `citar-file-note-extensions'."
+ (citar--check-configuration 'citar-notes-paths 'citar-file-note-extensions)
(citar-file--directory-files
citar-notes-paths keys citar-file-note-extensions
citar-file-additional-files-separator))
-(defun citar-file-has-notes (&optional _entries)
+(defun citar-file--has-notes ()
"Return predicate testing whether cite key has associated notes."
- ;; REVIEW why this optional arg when not needed?
- (let ((files (citar-file--get-notes-hash)))
- (lambda (key)
- (gethash key files))))
-
-(defun citar-file--open-note (key entry)
- "Open a note file from KEY and ENTRY."
- (if-let* ((file (citar-file--get-note-filename key
- citar-notes-paths
- citar-file-note-extensions))
- (file-exists (file-exists-p file)))
- (find-file file)
- (if (and (null citar-notes-paths)
- (equal (citar--get-notes-config :action)
- 'citar-org-format-note-default))
- (error "You must set 'citar-notes-paths'")
- (funcall
- (citar--get-notes-config :create) key entry))))
-
-(defun citar-file--get-note-files (keys)
+ (let ((files (citar-file--note-directory-files)))
+ (lambda (key) (gethash key files))))
+
+(defun citar-file--create-note (key entry)
+ "Create a note file from KEY and ENTRY."
+ (if-let ((filename (citar-file--get-note-filename key)))
+ (prog1 (find-file filename)
+ (unless (file-exists-p filename)
+ (citar--check-configuration 'citar-note-format-function)
+ (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 ((notehash (citar-file--get-notes-hash keys)))
- (flatten-list (map-values notehash))))
+ (let ((notes (citar-file--note-directory-files keys)))
+ (apply #'append (map-values notes))))
-(defun citar-file--get-note-filename (key dirs extensions)
- "Return existing or new filename for KEY in DIRS with extension in EXTENSIONS.
+(defun citar-file--get-note-filename (key)
+ "Return existing or new note filename for KEY.
This is for use in a note function where notes are one-per-file,
with citekey as filename.
Returns the filename whether or not the file exists, to support a
function that will open a new file if the note is not present."
- (let ((files (citar-file--directory-files dirs (list key) extensions
- citar-file-additional-files-separator)))
+ (citar--check-configuration 'citar-notes-paths 'citar-file-note-extensions)
+ (let* ((dirs citar-notes-paths)
+ (exts citar-file-note-extensions)
+ (files (citar-file--directory-files dirs (list key) exts citar-file-additional-files-separator)))
(or (car (gethash key files))
(when-let ((dir (car dirs))
- (ext (car extensions)))
+ (ext (car exts)))
(expand-file-name (concat key "." ext) dir)))))
;;;; Utility functions
diff --git a/citar-format.el b/citar-format.el
index f7a5ca82..44b97fd7 100644
--- a/citar-format.el
+++ b/citar-format.el
@@ -1,21 +1,7 @@
;;; citar-format.el --- Formatting functions for citar -*- lexical-binding: t; -*-
;;
-;; Copyright (C) 2022 Bruce D'Arcus, Roshan Shariff
-;;
-;; This file is not part of GNU Emacs.
-;;
-;; 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 .
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus, Roshan Shariff
+;; SPDX-License-Identifier: GPL-3.0-or-later
;;
;;; Commentary:
;;
diff --git a/citar-latex.el b/citar-latex.el
index 8b1ba3a8..3b62a859 100644
--- a/citar-latex.el
+++ b/citar-latex.el
@@ -1,28 +1,14 @@
;;; citar-latex.el --- Latex adapter for citar -*- lexical-binding: t; -*-
-;; Copyright (C) 2021 Bruce D'Arcus
-
-;; This file is not part of GNU Emacs.
-
-;; 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 .
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus
+;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
;; A small package that provides the functions required to use citar
;; with latex.
-;; Simply loading this file will enable manipulating the citations with
+;; Loading this file will enable manipulating the citations with
;; commands provided by citar.
;;; Code:
diff --git a/citar-markdown.el b/citar-markdown.el
index f10300c2..54e7b325 100644
--- a/citar-markdown.el
+++ b/citar-markdown.el
@@ -1,29 +1,14 @@
;;; citar-markdown.el --- Markdown adapter for citar -*- lexical-binding: t; -*-
-;; Copyright (C) 2021 Bruce D'Arcus
-
-;; This file is not part of GNU Emacs.
-
-;; 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 .
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus
+;; SPDX-License-Identifier: GPL-3.0-or-later
;;; Commentary:
-;; A small package that provides the functions required to use citar
-;; with markdown.
+;; A small package that provides functions required to use citar with markdown.
-;; Simply loading this file will enable manipulating the citations with
-;; commands provided by citar.
+;; Loading this file will enable manipulating the citations with commands
+;; provided by citar.
;;; Code:
diff --git a/citar-org.el b/citar-org.el
index 3e621bfb..3eaf52a7 100644
--- a/citar-org.el
+++ b/citar-org.el
@@ -1,22 +1,10 @@
;;; citar-org.el --- Org-cite support for citar -*- lexical-binding: t; -*-
-;; Copyright (C) 2021 Bruce D'Arcus
+;; SPDX-FileCopyrightText: 2021-2022 Bruce D'Arcus
+;; SPDX-License-Identifier: GPL-3.0-or-later
;; This file is not part of GNU Emacs.
-
-;; This program is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; This program is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-;; GNU General Public License for more details.
-
-;; You should have received a copy of the GNU General Public License
-;; along with this program. If not, see .
-
+;;
;;; Commentary:
;; This is a small package that integrates citar and org-cite. It
diff --git a/citar.el b/citar.el
index ae6c9394..a57cbf33 100644
--- a/citar.el
+++ b/citar.el
@@ -7,39 +7,24 @@
;; Created: February 27, 2021
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Version: 0.9.7
-;; Homepage: https://github.com/bdarcus/citar
+;; Homepage: https://github.com/emacs-citar/citar
;; Package-Requires: ((emacs "27.1") (parsebib "3.0") (org "9.5") (citeproc "0.9"))
;; This file is not part of GNU Emacs.
-
-;; This program is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; This program is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-;; GNU General Public License for more details.
-
-;; You should have received a copy of the GNU General Public License
-;; along with this program. If not, see .
-
+;;
;;; Commentary:
;; A completing-read front-end to browse, filter and act on BibTeX, BibLaTeX,
;; and CSL JSON bibliographic data, including LaTeX, markdown, and org-cite
;; citation editing support.
;;
-;; With embark, it also provides access to contextual actions, both in the
-;; minibuffer, and in the buffer at-point.
-
;;; Code:
(eval-when-compile
(require 'cl-lib)
(require 'subr-x))
(require 'seq)
+(require 'map)
(require 'browse-url)
(require 'citar-cache)
(require 'citar-format)
@@ -82,6 +67,27 @@
;;; Variables
+(defvar-local citar--entries nil
+ "Override currently active citar entries.
+
+When non-nil, should be a hash table mapping citation keys to
+entries, as returned by `citar-get-entries'. Then all citar
+functions will use that hash table as the source of bibliography
+data instead of accessing the cache.
+
+This variable should only be let-bound locally for the duration
+of individual functions or operations. This is useful when using
+multiple Citar functions in quick succession, to guarantee that
+all potential cache accesses and updates are performed up-front.
+In such cases, use a pattern like this:
+
+ (let ((citar--entries (citar-get-entries)))
+ ...)
+
+Note that this variable is buffer-local, since Citar has a
+different list of bibliographies (and hence entries) for each
+buffer.")
+
;;;; Faces
(defgroup citar nil
@@ -271,10 +277,11 @@ If nil, single resources will open without prompting."
`((citar-file .
,(list :name "Notes"
:category 'file
- :hasnote #'citar-file-has-notes
- :action #'citar-file--open-note
- :create #'citar-org-format-note-default ; TODO remove?
- :items #'citar-file--get-note-files)))
+ :items #'citar-file--get-notes
+ :hasitems #'citar-file--has-notes
+ :open #'find-file
+ :create #'citar-file--create-note
+ :transform #'file-name-nondirectory)))
"The alist of notes backends available for configuration.
The format of the cons should be (NAME . PLIST), where the
@@ -284,13 +291,15 @@ plist has the following properties:
:category the completion category
- :hasnote function to test for keys with notes
+ :hasitems function to test for keys with notes
- :action function to open a given note candidate
+ :open function to open a given note candidate
:items function to return candidate strings for keys
- :annotate annotation function (optional)"
+ :annotate annotation function (optional)
+
+ :transform transformation function (optional)"
:group 'citar
:type '(alist :key-type symbol :value-type plist))
@@ -299,6 +308,14 @@ plist has the following properties:
:group 'citar
:type 'symbol)
+;; TODO should this be a major mode function?
+(defcustom citar-note-format-function #'citar-org-format-note-default
+ "Function used by `citar-file' note source to format new notes."
+ :group 'citar
+ :type 'function)
+
+;;;; Major mode functions
+
;; TODO Move this to `citar-org', since it's only used there?
;; Otherwise it seems to overlap with `citar-default-action'
(defcustom citar-at-point-function #'citar-dwim
@@ -576,124 +593,136 @@ HISTORY is the `completing-read' history argument."
(equal item "")))))
(hash-table-keys selected-hash)))
-(defun citar--add-notep-prop (candidate)
- "Add a note resource CANDIDATE with 'notep t'."
- (propertize candidate 'notep t))
-
-(cl-defun citar--get-resource-candidates (keys &key files notes links)
- "Return related resource candidates for KEYS.
-
-Optionally constrain to FILES, NOTES, and/or LINKS."
- (let* ((filesource
- (when files
- (cons 'file
- (let ((citar-library-file-extensions nil))
- (citar-get-files keys)))))
- (linksource
- (when links
- (cons 'url (citar-get-links keys))))
- (notesource
- (when notes
- (let* ((cat (citar--get-notes-config :category))
- (items (citar--get-notes-config :items))
- (items (if (functionp items) (funcall items keys) items))
- (items (mapcar #'citar--add-notep-prop items)))
- (cons cat items))))
- (sources (list filesource linksource notesource))
- (candidates (list))
- ;; REVIEW initially I deleted nil sources, but I think that's overkill?
- (multicat (< 1 (length sources))))
- (progn
- (dolist (source sources)
- (let ((cat (car source)))
- (dolist (cand (cdr source))
- (push
- (if multicat
- (propertize cand 'multi-category (cons cat cand)) cand)
- candidates))))
- candidates)))
+(cl-defun citar--get-resource-candidates (key-or-keys &key files links notes)
+ "Return related resource candidates for KEY-OR-KEYS.
+
+Return a list (CATEGORY . CANDIDATES), where CATEGORY is a
+completion category and CANDIDATES is a list of resources
+associated with KEY-OR-KEYS. Return nil if there are no
+associated resources.
+
+The resources include:
+ * FILES: a list of files or t to use `citar-get-files'.
+ * LINKS: a list of links or t to use `citar-get-links'.
+ * NOTES: a list of notes or t to use `citar-get-notes'.
+
+If any of FILES, LINKS, or NOTES is nil, that resource type is
+omitted from CANDIDATES.
+
+CATEGORY is either `file' when returning only files, `url' when
+returning only links, or the category specified by
+`citar-notes-source' if returning only notes. When CANDIDATES has
+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)))
+ (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)))
+ (notecat (citar--get-notes-config :category))
+ (sources (nconc (when files (list (cons 'file (withtype 'file files))))
+ (when links (list (cons 'url (withtype 'url links))))
+ (when notes (list (cons notecat (withtype 'note notes)))))))
+ (if (null (cdr sources)) ; if sources is nil or singleton list,
+ (car sources) ; return either nil or the only source.
+ (cons 'multi-category ; otherwise, combine all sources
+ (mapcan
+ (pcase-lambda (`(,cat . ,cands))
+ (if (not cat)
+ cands
+ (mapcar (lambda (cand) (propertize cand 'multi-category (cons cat cand))) cands)))
+ sources))))))
(defun citar--annotate-note (candidate)
"Annotate note CANDIDATE."
- (when-let* ((notep (get-text-property 0 'notep candidate))
- (annotate (citar--get-notes-config :annotate)))
- (funcall
- annotate
- (substring-no-properties
- (cdr (get-text-property 0 'multi-category candidate))))))
-
-(cl-defun citar--select-resource (keys &optional &key files notes links)
+ (when-let (((eq 'note (get-text-property 0 'citar--resource candidate)))
+ (annotate (citar--get-notes-config :annotate)))
+ (funcall annotate (substring-no-properties candidate))))
+
+(cl-defun citar--select-resource (keys &key files notes links (always-prompt t))
;; FIX the arg list above is not smart
- "Select related FILES, NOTES, or LINKS resource for KEYS."
- (if-let ((resources
- (citar--get-resource-candidates
- keys :files files :notes notes :links links)))
- (completing-read
- "Select resource: "
- (lambda (string predicate action)
- (if (eq action 'metadata)
- `(metadata
- (group-function . citar--select-group-related-resources)
- (annotation-function . citar--annotate-note)
- (category . multi-category))
- (complete-with-action action resources string predicate))))))
+ "Select related FILES, NOTES, or LINKS resource for KEYS.
+
+Return (TYPE . RESOURCE), where TYPE is `file', `link', or `note'
+and RESOURCE is the selected resource string. Return nil if there
+are no resources.
+
+Use `completing-read' to prompt for a resource, unless there is
+only one resource and ALWAYS-PROMPT is nil. Return nil if the
+user declined to choose."
+ (when-let ((resources (citar--get-resource-candidates keys :files files :notes notes :links links)))
+ (pcase-let ((`(,category . ,cands) resources))
+ (when-let ((selected
+ (if (and (not always-prompt) (null (cdr cands)))
+ (car cands)
+ (let* ((metadata `(metadata
+ (group-function . ,#'citar--select-group-related-resources)
+ (annotation-function . ,#'citar--annotate-note)
+ ,@(when category `((category . ,category)))))
+ (table (lambda (string predicate action)
+ (if (eq action 'metadata)
+ metadata
+ (complete-with-action action cands string predicate))))
+ (selected (completing-read "Select resource: " table nil t)))
+ (car (member selected cands))))))
+ (cons (get-text-property 0 'citar--resource selected) (substring-no-properties selected))))))
(defun citar--select-group-related-resources (resource transform)
"Group RESOURCE by type or TRANSFORM."
- (if transform
- (if (file-regular-p resource)
- (file-name-nondirectory resource)
- resource)
- (let ((cat (car (get-text-property 0 'multi-category resource)))
- (notep (get-text-property 0 'notep resource)))
- ;; If note, assign to note group; otherwise use completion category.
- (if notep (citar--get-notes-config :name)
- (pcase cat
- ('file "Library Files")
- ('url "Links"))))))
-
-(cl-defun citar--format-candidates (&key (bibs (citar--bibliographies))
- (entries (citar-cache--entries bibs)))
- "Format completion candidates for ENTRIES.
-
-BIBS should be a list of `citar-cache--bibliography' objects that
-are the source of ENTRIES. Use the pre-formatted strings in BIBS
-to format candidates.
+ (pcase (get-text-property 0 'citar--resource resource)
+ ('file (if transform
+ (file-name-nondirectory resource)
+ "Library Files"))
+ ('url (if transform
+ resource
+ "Links"))
+ ('note (if transform
+ (funcall (or (citar--get-notes-config :transform) #'identity) resource)
+ (or (citar--get-notes-config :name) "Notes")))
+ (_ (if transform
+ resource
+ nil))))
+
+(defun citar--format-candidates ()
+ "Format completion candidates for bibliography entries.
Return a hash table with the keys being completion candidate
-strings and values being citation keys. Return nil if BIBS is
-nil."
+strings and values being citation keys.
+
+Return nil if `citar-bibliographies' returns nil."
;; Populate bibliography cache.
- (when bibs
- (let* ((preformatted (citar-cache--preformatted bibs))
- (hasfilesp (citar-has-files :entries entries))
- (hasnotesp (citar-has-notes :entries entries))
- (haslinksp (citar-has-links :entries entries))
+ (when-let ((bibs (citar--bibliographies)))
+ (let* ((citar--entries (citar-cache--entries bibs))
+ (preformatted (citar-cache--preformatted bibs))
+ (hasfilesp (citar-has-files))
+ (hasnotesp (citar-has-notes))
+ (haslinksp (citar-has-links))
(hasfilestag (propertize " has:files" 'invisible t))
(hasnotestag (propertize " has:notes" 'invisible t))
(haslinkstag (propertize " has:links" 'invisible t))
(symbolswidth (string-width (citar--symbols-string t t t)))
(width (- (frame-width) symbolswidth 2))
- (completions (make-hash-table :test 'equal :size (hash-table-count entries))))
- (maphash
- (lambda (citekey _entry)
- (let* ((hasfiles (and hasfilesp (funcall hasfilesp citekey)))
- (hasnotes (and hasnotesp (funcall hasnotesp citekey)))
- (haslinks (and haslinksp (funcall haslinksp citekey)))
- (preform (or (gethash citekey preformatted)
- (error "No preformatted candidate string: %s" citekey)))
- (display (citar-format--star-widths
- (- width (car preform)) (cdr preform)
- t citar-ellipsis))
- (tagged (if (not (or hasfiles hasnotes haslinks))
- display
- (concat display
- (when hasfiles hasfilestag)
- (when hasnotes hasnotestag)
- (when haslinks haslinkstag)))))
- (puthash tagged citekey completions)))
- entries)
- completions)))
+ (completions (make-hash-table :test 'equal :size (hash-table-count citar--entries))))
+ (prog1 completions
+ (maphash
+ (lambda (citekey _entry)
+ (let* ((hasfiles (and hasfilesp (funcall hasfilesp citekey)))
+ (hasnotes (and hasnotesp (funcall hasnotesp citekey)))
+ (haslinks (and haslinksp (funcall haslinksp citekey)))
+ (preform (or (gethash citekey preformatted)
+ (error "No preformatted candidate string: %s" citekey)))
+ (display (citar-format--star-widths
+ (- width (car preform)) (cdr preform)
+ t citar-ellipsis))
+ (tagged (if (not (or hasfiles hasnotes haslinks))
+ display
+ (concat display
+ (when hasfiles hasfilestag)
+ (when hasnotes hasnotestag)
+ (when haslinks haslinkstag)))))
+ (puthash tagged citekey completions)))
+ citar--entries)))))
(defun citar--extract-candidate-citekey (candidate)
"Extract the citation key from string CANDIDATE."
@@ -757,13 +786,15 @@ If no function is found, the DEFAULT function is called."
Note: this function accesses the bibliography cache and should
not be used for retreiving a large number of entries. Instead,
prefer `citar--get-entries'."
- (citar-cache--entry key (citar--bibliographies)))
+ (if citar--entries
+ (gethash key citar--entries)
+ (citar-cache--entry key (citar--bibliographies))))
(defun citar-get-entries ()
"Return all entries for currently active bibliographies.
Return a hash table whose keys are citation keys and values are
the corresponding entries."
- (citar-cache--entries (citar--bibliographies)))
+ (or citar--entries (citar-cache--entries (citar--bibliographies))))
(defun citar-get-value (field key-or-entry)
"Return value of FIELD in reference KEY-OR-ENTRY.
@@ -812,57 +843,63 @@ The value is transformed using `citar-display-transform-functions'"
"Register note backend.
NAME is a symbol, and CONFIG is a plist."
- (add-to-list 'citar-notes-sources (cons name config)))
+ (citar--check-notes-source name config)
+ (setf (alist-get name citar-notes-sources) config))
(defun citar-remove-notes-source (name)
"Remove note backend NAME."
- (assoc-delete-all name citar-notes-sources))
-
-(defun citar-get-notes (keys)
- "Return list of notes associated with KEYS."
- (funcall (citar--get-notes-config :items) keys))
-
-(cl-defun citar-get-files (key-or-keys &key (entries (citar-get-entries)))
- "Return list of files associated with KEY-OR-KEYS in ENTRIES.
-
-ENTRIES should be a hash table mapping elements of KEYS to
-bibliography entries. ENTRIES should also contain any items that
-are potentially cross-referenced from elements of KEYS.
-
-Find files using `citar-get-files-functions'."
- (when-let ((keys (citar--with-crossref-keys key-or-keys entries)))
- (delete-dups (mapcan (lambda (fn) (funcall fn keys entries)) citar-get-files-functions))))
-
-
-(cl-defun citar-get-links (key-or-keys &key (entries (citar-get-entries)))
- "Return list of links associated with KEY-OR-KEYS in ENTRIES.
-
-ENTRIES should be a hash table mapping elements of KEYS to
-bibliography entries. ENTRIES should also contain any items that
-are potentially cross-referenced from elements of KEYS."
- (delete-dups
- (mapcan
- (lambda (key)
- (when-let ((entry (gethash key entries)))
- (mapcan
- (pcase-lambda (`(,fieldname . ,baseurl))
- (when-let ((fieldvalue (citar-get-value fieldname entry)))
- (list (concat baseurl fieldvalue))))
- '((doi . "https://doi.org/")
- (pmid . "https://www.ncbi.nlm.nih.gov/pubmed/")
- (pmcid . "https://www.ncbi.nlm.nih.gov/pmc/articles/")
- (url . nil)))))
- (citar--with-crossref-keys key-or-keys entries))))
-
-
-(cl-defun citar-has-files (&key (entries (citar-get-entries)))
+ (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)))))
+
+(defun citar-create-note (key &optional entry)
+ "Create a note for KEY and ENTRY.
+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))))
+
+
+(defun citar-has-files ()
"Return predicate testing whether entry has associated files.
Return a function that takes KEY and returns non-nil when the
-corresponding entry in ENTRIES has associated files. ENTRIES
-should be a hash table mapping citation keys to entries, as
-returned by `citar-get-entries'. The returned predicated may by
-nil if no entries have associated files.
+corresponding bibliography entry has associated files. The
+returned predicated may by nil if no entries have associated
+files.
For example, to test whether KEY has associated files:
@@ -874,26 +911,18 @@ 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'.
+from the given KEY; see `citar-crossref-variable'."
+ (citar--has-resources
+ (mapcar #'funcall citar-has-files-functions)))
-Note: All the potentially cross-referenced entries should be
-present in ENTRIES. In most cases, ENTRIES should be its default
-value (the result of `citar-get-entries') rather than some
-smaller subset."
- (citar--has-resources-for-entries
- entries
- (mapcar (lambda (fn) (funcall fn entries))
- citar-has-files-functions)))
-
-(cl-defun citar-has-notes (&key (entries (citar-get-entries)))
+(defun citar-has-notes ()
"Return predicate testing whether entry has associated notes.
Return a function that takes KEY and returns non-nil when the
-corresponding entry in ENTRIES has associated notes. ENTRIES
-should be a hash table mapping citation keys to entries, as
-returned by `citar-get-entries'. The returned predicate may be
-nil if no entries have associated notes.
+corresponding bibliography entry has associated notes. The
+returned predicate may be nil if no entries have associated
+notes.
For example, to test whether KEY has associated notes:
@@ -905,34 +934,24 @@ returned predicate repeatedly.
Notes are detected using `citar-has-notes-functions', which see.
Also check any bibliography entries that are cross-referenced
-from the given KEY; see `citar-crossref-variable'.
-
-Note: All the potentially cross-referenced entries should be
-present in ENTRIES. In most cases, ENTRIES should be its default
-value (the result of `citar-get-entries') rather than some
-smaller subset."
- (citar--has-resources-for-entries
- entries
- (funcall
- (citar--get-notes-config :hasnote) entries)))
+from the given KEY; see `citar-crossref-variable'."
+ (citar--has-resources
+ (funcall (citar--get-notes-config :hasitems))))
-(cl-defun citar-has-links (&key (entries (citar-get-entries)))
+(defun citar-has-links ()
"Return predicate testing whether entry has links.
Return a function that takes KEY and returns non-nil when the
-corresponding entry in ENTRIES has associated links. See 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-for-entries
- entries
- (lambda (key)
- (when-let ((entry (gethash key entries)))
- (citar-get-field-with-value '(doi pmid pmcid url) entry)))))
+ (citar--has-resources
+ (apply-partially #'citar-get-field-with-value '(doi pmid pmcid url))))
-(defun citar--has-resources-for-entries (entries predicates)
- "Return predicate combining results of calling FUNCTIONS.
+(defun citar--has-resources (predicates)
+ "Combine PREDICATES into a single resource predicate.
PREDICATES should be a list of functions that take a bibliography
KEY and return non-nil if the item has a resource. It may also be
@@ -960,8 +979,7 @@ another entry in ENTRIES that has associated resources."
(if-let ((xref citar-crossref-variable))
(lambda (citekey)
(or (funcall hasresourcep citekey)
- (when-let ((entry (gethash citekey entries))
- (xkey (citar-get-value xref entry)))
+ (when-let ((xkey (citar-get-value xref citekey)))
(funcall hasresourcep xkey))))
hasresourcep)))
@@ -1008,26 +1026,24 @@ personal names of the form \"family, given\"."
(list citar-crossref-variable))
. ,citar-additional-fields)))
-(defun citar--with-crossref-keys (key-or-keys entries)
- "Return KEY-OR-KEYS augmented with cross-referenced items in ENTRIES.
+(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 key in ENTRIES, if any.
+cross-referenced keys, if any.
-ENTRIES should be a hash table mapping elements of KEYS to
-bibliography entries. ENTRIES should also contain any items that
-are potentially cross-referenced from elements of KEYS."
+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))))
- (if (not xref)
- keys
- (mapcan (lambda (key)
- (cons key (if-let* ((entry (gethash key entries))
- (xkey (citar-get-value xref entry)))
- (list xkey))))
- 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
@@ -1105,28 +1121,24 @@ are potentially cross-referenced from elements of KEYS."
(defun citar-open (keys)
"Open related resources (links or files) for KEYS."
(interactive (list (citar-select-refs)))
- (let ((resource-candidates
- (citar--get-resource-candidates
- keys :files t :notes t :links t)))
- (cond
- ((eq nil resource-candidates)
- (error "No associated resources"))
- ((unless citar-open-prompt
- (eq 1 (length resource-candidates)))
- (citar--open-multi (car resource-candidates)))
- (t (citar--open-multi
- (citar--select-resource keys :files t :notes t :links t))))))
-
-(defun citar--open-multi (selection)
- "Act appropriately on SELECTION when type is `multi-category'.
-For use with `embark-act-all'."
- (cond ((string-match "http" selection 0)
- (browse-url selection))
- ((member t (mapcar (lambda (x)
- (file-in-directory-p selection x))
- citar-notes-paths))
- (find-file selection))
- (t (citar-file-open selection))))
+ (if-let ((selected (let* ((actions (bound-and-true-p embark-default-action-overrides))
+ (embark-default-action-overrides `((t . ,#'citar--open-resource) . ,actions)))
+ (citar--select-resource keys :files t :links t :notes t
+ :always-prompt citar-open-prompt))))
+ (citar--open-resource (cdr selected) (car selected))
+ (error "No associated resources: %s" keys)))
+
+(defun citar--open-resource (resource &optional type)
+ "Open RESOURCE of TYPE, which should be `file', `url', or `note'.
+If TYPE is nil, then RESOURCE must have a `citar--resource' text
+property specifying TYPE."
+ (if-let* ((type (or type (get-text-property 0 'citar--resource resource)))
+ (open (pcase type
+ ('file #'citar-file-open)
+ ('url #'browse-url)
+ ('note (citar--get-notes-config :open)))))
+ (funcall open (substring-no-properties resource))
+ (error "Could not open resource of type `%s': %S" type resource)))
;; TODO Rename? This also opens files in bib field, not just library files
;;;###autoload
@@ -1134,32 +1146,33 @@ For use with `embark-act-all'."
"Open library file associated with KEY-OR-KEYS."
(interactive (list (citar-select-refs)))
;; TODO filter to refs have files?
- (let ((embark-default-action-overrides '((file . citar-file-open))))
- (citar--library-file-action key-or-keys #'citar-file-open)))
+ (citar--library-file-action key-or-keys #'citar-file-open))
;;;###autoload
(defun citar-attach-files (key-or-keys)
"Attach library file associated with KEY-OR-KEYS to outgoing MIME message."
(interactive (list (citar-select-ref)))
- (let ((embark-default-action-overrides '((file . mml-attach-file))))
- (citar--library-file-action key-or-keys #'mml-attach-file)))
+ (citar--library-file-action key-or-keys #'mml-attach-file))
(defun citar--library-file-action (key-or-keys action)
- "Run ACTION on file associated with KEY-OR-KEYS."
- (let ((entries (citar-get-entries)))
- (if-let ((files (citar-get-files key-or-keys :entries entries)))
- (funcall action (if (null (cdr files))
- (car files)
- ;; REVIEW this function will return files for keys
- ;; also, candidates are mult-category, even though only one
- (citar--select-resource key-or-keys :files t)))
+ "Run ACTION on file associated with KEY-OR-KEYS.
+If KEY-OR-KEYS have multiple files, use `completing-read' to
+select a single file."
+ (let ((citar--entries (citar-get-entries)))
+ (if-let ((resource (let* ((actions (bound-and-true-p embark-default-action-overrides))
+ (embark-default-action-overrides `(((file . ,this-command) . ,action)
+ . ,actions)))
+ (citar--select-resource key-or-keys :files t))))
+ (if (eq 'file (car resource))
+ (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.
(let ((keys (if (listp key-or-keys) key-or-keys (list key-or-keys)))
- (hasfilep (citar-has-files :entries entries)))
+ (hasfilep (citar-has-files)))
(unless (and hasfilep (seq-some hasfilep keys))
(message "No associated files for %s" key-or-keys)))))))
@@ -1167,19 +1180,28 @@ For use with `embark-act-all'."
(defun citar-open-notes (keys)
"Open notes associated with the KEYS."
(interactive (list (citar-select-refs)))
- (dolist (key keys)
- (let ((entry (citar-get-entry key)))
- (funcall
- (citar--get-notes-config :action) key entry))))
+ (if-let ((notes (citar-get-notes keys)))
+ (progn (mapc (citar--get-notes-config :open) notes)
+ (let ((count (length notes)))
+ (when (> count 1)
+ (message "Opened %d notes" count))))
+ (when keys
+ (if (null (cdr keys))
+ (citar-create-note (car keys))
+ (message "No notes found. Select one key to create note: %s" keys)))))
;;;###autoload
-(defun citar-open-links (keys)
- "Open URL or DOI link associated with KEYS in a browser."
+(defun citar-open-links (key-or-keys)
+ "Open URL or DOI link associated with KEY-OR-KEYS in a browser."
(interactive (list (citar-select-refs)))
- ;; REVIEW this works, but should check for nil on select-resource
- (if-let ((link (citar--select-resource keys :links t)))
- (browse-url link)
- (message "No link found for %s" keys)))
+ (if-let ((resource (let* ((actions (bound-and-true-p embark-default-action-overrides))
+ (embark-default-action-overrides `(((url . ,this-command) . ,#'browse-url)
+ . ,actions)))
+ (citar--select-resource key-or-keys :links t))))
+ (if (eq 'url (car resource))
+ (browse-url (cdr resource))
+ (error "Expected resource of type `url', got `%s': %S" (car resource) (cdr resource)))
+ (message "No link found for %s" key-or-keys)))
;;;###autoload
(defun citar-open-entry (key)
@@ -1359,18 +1381,40 @@ VARIABLES should be the names of Citar customization variables."
(unless (and (listp value)
(seq-every-p #'stringp value))
(error "`%s' should be a list of directories: %S" variable `',value)))
- ((or 'citar-library-file-extensions)
+ ((or 'citar-library-file-extensions 'citar-file-note-extensions)
(unless (and (listp value)
(seq-every-p #'stringp value))
(error "`%s' should be a list of strings: %S" variable `',value)))
- ((or 'citar-has-files-functions 'citar-get-files-functions
- ; (citar--get-notes-config :hasnote)
- ; (citar--get-notes-config :action)
- 'citar-file-parser-functions)
+ ((or 'citar-has-files-functions 'citar-get-files-functions 'citar-file-parser-functions)
(unless (and (listp value) (seq-every-p #'functionp value))
(error "`%s' should be a list of functions: %S" variable `',value)))
+ ((or 'citar-note-format-function)
+ (unless (functionp value)
+ (error "`%s' should be a function: %S" variable `',value)))
(_
(error "Unknown variable in citar--check-configuration: %s" variable))))))
+(defun citar--check-notes-source (name config)
+ "Signal error if notes source plist CONFIG has incorrect keys or values.
+SOURCE must be a plist representing a notes source with NAME. See
+`citar-notes-sources' for the list of valid keys and types."
+
+ (let ((required '(:items :hasitems :open))
+ (optional '(:name :category :create :transform :annotate))
+ (keys (map-keys config)))
+ (when-let ((missing (cl-set-difference required keys)))
+ (error "Note source `%s' missing required keys: %s" name missing))
+ (when-let ((extra (cl-set-difference keys (append required optional))))
+ (warn "Note source `%s' has unknown keys: %s" name extra)))
+
+ (pcase-dolist (`(,type . ,props)
+ '((functionp :items :hasitems :open :create :transform :annotate)
+ (stringp :name)
+ (symbolp :category)))
+ (when-let ((wrongtype (seq-filter (lambda (prop)
+ (when-let ((value (plist-get config prop)))
+ (not (funcall type value)))) props)))
+ (error "Note source `%s' keys must be of type %s: %s" name type wrongtype))))
+
(provide 'citar)
;;; citar.el ends here
diff --git a/test/citar-file-test.el b/test/citar-file-test.el
index 789fefb0..0e9b4c00 100644
--- a/test/citar-file-test.el
+++ b/test/citar-file-test.el
@@ -46,11 +46,10 @@
(ert-deftest citar-file-test--parse-file-field ()
- (let* ((fieldname "file")
+ (let* ((citar-file-variable "file")
(citekey "foo")
- (entry '((file . "foo.pdf")))
+ (fieldvalue "foo.pdf")
(dirs '("/home/user/library/"))
- (citar-file-variable fieldname)
(citar-file-parser-functions (list #'citar-file--parser-default))
lastmessage)
@@ -66,20 +65,20 @@
(and (equal "pdf" (file-name-extension filename))
(member (file-name-directory filename) dirs)))))
- (should-not (citar-file--parse-file-field '((file . " ")) dirs citekey))
+ (should-not (citar-file--parse-file-field " " dirs citekey))
(should (string=
(current-message)
- (format-message "Empty `%s' field: %s" fieldname citekey)))
+ (format-message "Empty `%s' field: %s" citar-file-variable citekey)))
(let ((citar-file-parser-functions nil))
- (should-not (citar-file--parse-file-field entry dirs citekey))
+ (should-not (citar-file--parse-file-field fieldvalue dirs citekey))
(should (string=
(current-message)
(format-message
"Could not parse `%s' field of `%s'; check `citar-file-parser-functions': %s"
- fieldname citekey (alist-get 'file entry)))))
+ citar-file-variable citekey fieldvalue))))
- (should-not (citar-file--parse-file-field '((file . "foo.html")) dirs citekey))
+ (should-not (citar-file--parse-file-field "foo.html" dirs citekey))
(should (string=
(current-message)
(format-message
@@ -88,7 +87,7 @@
citekey '("foo.html"))))
(let ((citar-library-file-extensions '("html")))
- (should-not (citar-file--parse-file-field entry dirs citekey))
+ (should-not (citar-file--parse-file-field fieldvalue dirs citekey))
(should (string=
(current-message)
(format-message
@@ -96,11 +95,11 @@
citekey '("/home/user/library/foo.pdf")))))
(let ((citar-library-file-extensions nil))
- (should (equal (citar-file--parse-file-field entry dirs citekey)
+ (should (equal (citar-file--parse-file-field fieldvalue dirs citekey)
'("/home/user/library/foo.pdf"))))
(let ((citar-library-file-extensions '("pdf" "html")))
- (should (equal (citar-file--parse-file-field entry dirs citekey)
+ (should (equal (citar-file--parse-file-field fieldvalue dirs citekey)
'("/home/user/library/foo.pdf")))))))
(provide 'citar-file-test)
diff --git a/test/citar-test.el b/test/citar-test.el
new file mode 100644
index 00000000..956b5dfb
--- /dev/null
+++ b/test/citar-test.el
@@ -0,0 +1,16 @@
+;;; citar-file-test.el --- Tests for citar.el -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;;; Code:
+
+(require 'ert)
+(require 'citar)
+(require 'map)
+
+(ert-deftest citar-test--check-notes-sources ()
+ ;; This should run without signalling an error
+ (should-not (ignore (map-do #'citar--check-notes-source citar-notes-sources))))
+
+(provide 'citar-test)
+;;; citar-test.el ends here