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