Compare commits

..

No commits in common. "main" and "0.2.1" have entirely different histories.
main ... 0.2.1

11 changed files with 2500 additions and 20545 deletions

File diff suppressed because it is too large Load diff

View file

@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) 2021 Protesilaos
Copyright (C) 2021 Protesilaos Stavrou
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
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) 2021 Protesilaos
<program> Copyright (C) 2021 Protesilaos Stavrou
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.

View file

@ -1,29 +0,0 @@
# denote: Simple notes with an efficient file-naming scheme
New to Denote? Start reading this from the manual:
<https://protesilaos.com/emacs/denote#h:c54bedb4-5377-4dbd-853c-5870ace6eb33>.
* * *
Denote is a simple note-taking tool for Emacs. It is based on the idea
that notes should follow a predictable and descriptive file-naming
scheme. The file name must offer a clear indication of what the note is
about, without reference to any other metadata. Denote basically
streamlines the creation of such files while providing facilities to
link between them.
Denote's file-naming scheme is not limited to "notes". It can be used
for all types of file, including those that are not editable in Emacs,
such as videos. Naming files in a consistent way makes their
filtering and retrieval considerably easier. Denote provides relevant
facilities to rename files, regardless of file type.
+ Package name (GNU ELPA): `denote`
+ Official manual: <https://protesilaos.com/emacs/denote>
+ Change log: <https://protesilaos.com/emacs/denote-changelog>
+ Git repositories:
+ GitHub: <https://github.com/protesilaos/denote>
+ GitLab: <https://gitlab.com/protesilaos/denote>
+ Video demo: <https://protesilaos.com/codelog/2022-06-18-denote-demo/>
+ Backronyms: Denote Everything Neatly; Omit The Excesses. Don't Ever
Note Only The Epiphenomenal.

6800
README.org

File diff suppressed because it is too large Load diff

388
denote-dired.el Normal file
View file

@ -0,0 +1,388 @@
;;; denote-dired.el --- Integration between Denote and Dired -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
;; URL: https://git.sr.ht/~protesilaos/denote
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
;; Version: 0.2.1
;; Package-Requires: ((emacs "27.2"))
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Denote's file-naming scheme is not specific to notes or text files:
;; it is useful for all sorts of files, such as multimedia and PDFs that
;; form part of the user's longer-term storage (read manual's "The
;; file-naming scheme"). While Denote does not manage such files, it
;; already has all the mechanisms to facilitate the task of renaming
;; them.
;;
;; To this end, we provide the `denote-dired-rename-file' command. It
;; has a two-fold purpose: (i) to change the name of an existing file
;; while retaining its identifier and (ii) to write a Denote-compliant
;; file name for an item that was not created by `denote' or related
;; commands (such as an image or PDF).
;;
;; The `denote-dired-rename-file' command will target the file at point
;; if it finds one in the current Dired buffer. Otherwise it prompts
;; with minibuffer completion for a file name. It then uses the
;; familiar prompts for a `TITLE' and `KEYWORDS' the same way the
;; `denote' command does (read manual's "Points of entry). As a final
;; step, it asks for confirmation before renaming the file at point,
;; showing a message like:
;;
;; Rename sample.pdf to 20220612T052900--my-sample-title__testing.pdf? (y or n)
;;
;; However, if the user option `denote-dired-rename-expert' is non-nil,
;; conduct the renaming operation outright---no questions asked.
;;
;; When operating on a file that has no identifier, such as
;; `sample.pdf', Denote reads the file properties to retrieve its last
;; modification time. If the file was from a past date like 2000-11-31
;; it will get an identifier starting with `20001131' followed by the
;; time component (per our file-naming scheme).
;;
;; The file type extension (e.g. `.pdf') is read from the underlying
;; file and is preserved through the renaming process. Files that have
;; no extension are simply left without one.
;;
;; Renaming only occurs relative to the current directory. Files are not
;; moved between directories.
;;
;; The final step of the `denote-dired-rename-file' command is to call
;; the special hook `denote-dired-post-rename-functions'. Functions
;; added to that hook must accept three arguments, as explained in its
;; doc string. For the time being, the only function we define is the
;; one which updates the underlying note's front matter to match the new
;; file name: `denote-dired-rewrite-front-matter'. The function takes
;; care to only operate on an actual note, instead of arbitrary files.
;;
;; DEVELOPMENT NOTE: the `denote-dired-rewrite-front-matter' needs to be
;; tested thoroughly. It rewrites file contents so we have to be sure
;; it does the right thing. To avoid any trouble, it always asks for
;; confirmation before performing the replacement. This confirmation
;; ignores `denote-dired-rename-expert' for the time being, though we
;; might want to lift that restriction once everything works as
;; intended.
;;
;;
;; One of the upsides of Denote's file-naming scheme is the predictable
;; pattern it establishes, which appears as a near-tabular presentation in
;; a listing of notes (i.e. in Dired). The `denote-dired-mode' can help
;; enhance this impression, by fontifying the components of the file name
;; to make the date (identifier) and keywords stand out.
;;
;; There are two ways to set the mode. Either use it for all directories,
;; which probably is not needed:
;;
;; (require 'denote-dired)
;; (add-hook 'dired-mode-hook #'denote-dired-mode)
;;
;; Or configure the user option `denote-dired-directories' and then set up
;; the function `denote-dired-mode-in-directories':
;;
;; (require 'denote-dired)
;;
;; ;; We use different ways to specify a path for demo purposes.
;; (setq denote-dired-directories
;; (list denote-directory
;; (thread-last denote-directory (expand-file-name "attachments"))
;; (expand-file-name "~/Documents/vlog")))
;;
;; (add-hook 'dired-mode-hook #'denote-dired-mode-in-directories)
;;
;; The `denote-dired-mode' does not only fontify note files that were
;; created by Denote: it covers every file name that follows our naming
;; conventions (read about "The file-naming scheme" in the manual).
;; This is particularly useful for scenaria where, say, one wants to
;; organise their collection of PDFs and multimedia in a systematic way
;; (and, perhaps, use them as attachments for the notes Denote
;; produces).
;;
;; For the time being, the `diredfl' package is not compatible with this
;; facility.
;;; Code:
(require 'denote-retrieve)
(require 'dired)
(defgroup denote-dired ()
"Integration between Denote and Dired."
:group 'denote)
(defcustom denote-dired-directories
;; We use different ways to specify a path for demo purposes.
(list denote-directory
;; (thread-last denote-directory (expand-file-name "attachments"))
(expand-file-name "~/Documents/vlog"))
"List of directories where `denote-dired-mode' should apply to."
:type '(repeat directory)
:group 'denote-dired)
(defcustom denote-dired-rename-expert nil
"If t, `denote-dired-rename-file' doesn't ask for confirmation.
The confiration is asked via a `y-or-n-p' prompt which shows the
old name followed by the new one."
:type 'boolean
:group 'denote-dired)
(defcustom denote-dired-post-rename-functions
(list #'denote-dired-update-dired-buffers
#'denote-dired-rewrite-front-matter)
"List of functions called after `denote-dired-rename-file'.
Each function must accept three arguments: FILE, TITLE, and
KEYWORDS. The first is the full path to the file provided as a
string, the second is the human-readable file name (not what
Denote sluggifies) also as a string, and the third are the
keywords. If there is only one keyword, it is a string, else a
list of strings.
DEVELOPMENT NOTE: the `denote-dired-rewrite-front-matter' needs
to be tested thoroughly. It rewrites file contents so we have to
be sure it does the right thing. To avoid any trouble, it always
asks for confirmation before performing the replacement. This
confirmation ignores `denote-dired-rename-expert' for the time
being, though we might want to lift that restriction once
everything works as intended."
:type 'hook
:group 'denote-dired)
;;;; Commands
(defun denote-dired--file-attributes-time (file)
"Return `file-attribute-modification-time' of FILE as identifier."
(format-time-string
denote--id-format
(file-attribute-modification-time (file-attributes file))))
(defun denote-dired--file-name-id (file)
"Return FILE identifier, else generate one."
(cond
((string-match denote--id-regexp file)
(substring file (match-beginning 0) (match-end 0)))
((denote-dired--file-attributes-time file))
(t (format-time-string denote--id-format))))
(defun denote-dired--rename-buffer (old-name new-name)
"Rename OLD-NAME buffer to NEW-NAME, when appropriate."
(when-let* ((buffer (find-buffer-visiting old-name)))
(with-current-buffer buffer
(set-visited-file-name new-name nil t))))
(defun denote-dired--rename-dired-file-or-prompt ()
"Return Dired file at point, else prompt for one."
(or (dired-get-filename nil t)
(let* ((file (buffer-file-name))
(format (if file
(format "Rename file Denote-style [%s]: " file)
"Rename file Denote-style: ")))
(read-file-name format nil file t nil))))
(defun denote-dired--rename-file-is-regular (file)
"Throw error is FILE is not regular, else return FILE."
(if (or (file-directory-p file)
(not (file-regular-p file)))
(user-error "Only rename regular files")
file))
;;;###autoload
(defun denote-dired-rename-file (file title keywords)
"Rename FILE to include TITLE and KEYWORDS.
If in Dired, consider FILE to be the one at point, else prompt
with completion.
If FILE has a Denote-compliant identifier, retain it while
updating the TITLE and KEYWORDS fields of the file name. Else
create an identifier based on the file's attribute of last
modification time. If such attribute cannot be found, the
identifier falls back to the current time.
As a final step, prompt for confirmation, showing the difference
between old and new file names. If `denote-dired-rename-expert'
is non-nil, conduct the renaming operation outright---no
questions asked!
The file type extension (e.g. .pdf) is read from the underlying
file and is preserved through the renaming process. Files that
have no extension are simply left without one.
Renaming only occurs relative to the current directory. Files
are not moved between directories. As a final step, call the
`denote-dired-post-rename-functions'.
This command is intended to (i) rename existing Denote
notes, (ii) complement note-taking, such as by renaming
attachments that the user adds to their notes."
(interactive
(let ((file (denote-dired--rename-file-is-regular (denote-dired--rename-dired-file-or-prompt))))
(list
file
(denote--title-prompt (denote-retrieve--value-title file))
(denote--keywords-prompt))))
(let* ((dir (file-name-directory file))
(old-name (file-name-nondirectory file))
(extension (file-name-extension file t))
(new-name (denote--format-file
dir
(denote-dired--file-name-id file)
keywords
(denote--sluggify title)
extension))
(max-mini-window-height 0.33)) ; allow minibuffer to be resized
(unless (string= old-name (file-name-nondirectory new-name))
(when (y-or-n-p
(format "Rename %s to %s?"
(propertize old-name 'face 'error)
(propertize (file-name-nondirectory new-name) 'face 'success)))
(rename-file old-name new-name nil)
(denote-dired--rename-buffer old-name new-name)
(run-hook-with-args 'denote-dired-post-rename-functions new-name title keywords)))))
(defun denote-dired-update-dired-buffers (&rest _)
"Update Dired buffers of variable `denote-directory'.
Can run after `denote-dired-post-rename-functions', though it
ignores all its arguments."
(mapc
(lambda (buf)
(with-current-buffer buf
(when (and (eq major-mode 'dired-mode)
(string-match-p (expand-file-name default-directory)
(expand-file-name (denote-directory))))
(revert-buffer))))
(buffer-list)))
(defun denote-dired--file-meta-header (title date keywords id filetype)
"Front matter for renamed notes.
TITLE, DATE, KEYWORDS, FILENAME, ID, and FILETYPE are all strings
which are provided by `denote-dired-rewrite-front-matter'."
(let ((kw-space (denote--file-meta-keywords keywords))
(kw-toml (denote--file-meta-keywords keywords 'toml)))
(pcase filetype
('markdown-toml (format denote-toml-front-matter title date kw-toml id))
('markdown-yaml (format denote-yaml-front-matter title date kw-space id))
('text (format denote-text-front-matter title date kw-space id denote-text-front-matter-delimiter))
(_ (format denote-org-front-matter title date kw-space id)))))
(defun denote-dired--filetype-heuristics (file)
"Return likely file type of FILE.
The return value is for `denote--file-meta-header'."
(pcase (file-name-extension file)
("md" (if (string-match-p "title\\s-*=" (denote-retrieve--value-title file t))
'markdown-toml
'markdown-yaml))
("txt" 'text)
(_ 'org)))
(defun denote-dired--front-matter-search-delimiter (filetype)
"Return likely front matter delimiter search for FILETYPE."
(pcase filetype
('markdown-toml (re-search-forward "^\\+\\+\\+$" nil t 2))
('markdown-yaml (re-search-forward "^---$" nil t 2))
;; 2 at most, as the user might prepend it to the block as well.
;; Though this might give us false positives, it ultimately is the
;; user's fault.
('text (or (re-search-forward denote-text-front-matter-delimiter nil t 2)
(re-search-forward denote-text-front-matter-delimiter nil t 1)
(re-search-forward "^[\s\t]*$" nil t 1)))
;; Org does not have a real delimiter. This is the trickiest one.
(_ (re-search-forward "^[\s\t]*$" nil t 1))))
(defun denote-dired--edit-front-matter-p (file)
"Test if FILE should be subject to front matter rewrite.
This is relevant for `denote-dired-rewrite-front-matter': if FILE
has no front matter, then we abort early instead of trying to
replace what isn't there."
(when-let ((ext (file-name-extension file)))
(and (file-regular-p file)
(file-writable-p file)
(not (denote--file-empty-p file))
(string-match-p "\\(md\\|org\\|txt\\)\\'" ext)
;; Heuristic to check if this is one of our notes
(string= (expand-file-name default-directory) (denote-directory)))))
(defun denote-dired-rewrite-front-matter (file title keywords)
"Rewrite front matter of note after `denote-dired-rename-file'.
The FILE, TITLE, and KEYWORDS are passed from the renaming
command and are used to construct a new front matter block if
appropriate."
(when-let* ((denote-dired--edit-front-matter-p file)
(id (denote-retrieve--filename-identifier file))
(date (denote-retrieve--value-date file))
(filetype (denote-dired--filetype-heuristics file))
(new-front-matter (denote--file-meta-header title date keywords id filetype)))
(let (old-front-matter front-matter-delimiter)
(with-current-buffer (find-file-noselect file)
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(setq front-matter-delimiter (denote-dired--front-matter-search-delimiter filetype))
(when front-matter-delimiter
(setq old-front-matter
(buffer-substring-no-properties
(point-min)
(progn front-matter-delimiter (point)))))))
(when (and old-front-matter
(y-or-n-p
(format "%s\n%s\nReplace front matter?"
(propertize old-front-matter 'face 'error)
(propertize new-front-matter 'face 'success))))
(delete-region (point-min) front-matter-delimiter)
(goto-char (point-min))
(insert new-front-matter)
;; FIXME 2022-06-16: Instead of `delete-blank-lines', we
;; should check if we added any new lines and delete only
;; those.
(delete-blank-lines))))))
;;;; Extra fontification
(require 'denote-faces)
;;;###autoload
(define-minor-mode denote-dired-mode
"Fontify all Denote-style file names in Dired."
:global nil
:group 'denote-dired
(if denote-dired-mode
(font-lock-add-keywords nil denote-faces-file-name-keywords t)
(font-lock-remove-keywords nil denote-faces-file-name-keywords))
(font-lock-flush (point-min) (point-max)))
(defun denote-dired--modes-dirs-as-dirs ()
"Return `denote-dired-directories' as directories.
The intent is to basically make sure that however a path is
written, it is always returned as a directory."
(mapcar
(lambda (dir)
(file-name-as-directory (file-truename dir)))
denote-dired-directories))
;;;###autoload
(defun denote-dired-mode-in-directories ()
"Enable `denote-dired-mode' in `denote-dired-directories'.
Add this function to `dired-mode-hook'."
(when (member (file-truename default-directory) (denote-dired--modes-dirs-as-dirs))
(denote-dired-mode 1)))
(provide 'denote-dired)
;;; denote-dired.el ends here

105
denote-faces.el Normal file
View file

@ -0,0 +1,105 @@
;;; denote-faces.el --- Faces and fontification rules for Denote -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
;; URL: https://git.sr.ht/~protesilaos/denote
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
;; Version: 0.2.1
;; Package-Requires: ((emacs "27.2"))
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Used internally by Denote to fontify file names in Dired, the
;; backlinks' buffer, and related.
;;; Code:
(require 'denote)
(defgroup denote-faces ()
"Faces for Denote."
:group 'denote)
(defface denote-faces-subdirectory
'((t :inherit bold))
"Face for subdirectory of file name.
This should only ever needed in the backlinks' buffer (or
equivalent), not in Dired."
:group 'denote-faces)
(defface denote-faces-date
'((t :inherit font-lock-variable-name-face))
"Face for file name date in Dired buffers.
This is the part of the identifier that covers the year, month,
and day."
:group 'denote-faces)
(defface denote-faces-time
'((t :inherit denote-faces-date))
"Face for file name time in Dired buffers.
This is the part of the identifier that covers the hours, minutes,
and seconds."
:group 'denote-faces)
(defface denote-faces-title
'((t ))
"Face for file name title in Dired buffers."
:group 'denote-faces)
(defface denote-faces-extension
'((t :inherit shadow))
"Face for file extension type in Dired buffers."
:group 'denote-faces)
(defface denote-faces-keywords
'((t :inherit font-lock-builtin-face))
"Face for file name keywords in Dired buffers."
:group 'denote-faces)
(defface denote-faces-delimiter
'((((class color) (min-colors 88) (background light))
:foreground "gray70")
(((class color) (min-colors 88) (background dark))
:foreground "gray30")
(t :inherit shadow))
"Face for file name delimiters in Dired buffers."
:group 'denote-faces)
(defconst denote-faces-file-name-keywords
`((,denote--file-regexp
(1 'denote-faces-date)
(2 'denote-faces-time)
(3 'denote-faces-delimiter)
(4 'denote-faces-title)
(5 'denote-faces-delimiter)
(6 'denote-faces-keywords)
(7 'denote-faces-extension))
("_"
(0 'denote-faces-delimiter t)))
"Keywords for fontification of file names.")
(defconst denote-faces-file-name-with-subdir-keywords
(append denote-faces-file-name-keywords
'(("\\(^.*/\\)?"
(0 'denote-faces-subdirectory))))
"Keywords for fontification of file names with a directory.")
(provide 'denote-faces)
;;; denote-faces.el ends here

556
denote-link.el Normal file
View file

@ -0,0 +1,556 @@
;;; denote-link.el --- Link facility for Denote -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
;; URL: https://git.sr.ht/~protesilaos/denote
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
;; Version: 0.2.1
;; Package-Requires: ((emacs "27.2"))
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; The `denote-link' command inserts a link at point to an entry specified
;; at the minibuffer prompt. Links are formatted depending on the file
;; type of current note. In Org and plain text buffers, links are
;; formatted thus: `[[denote:IDENTIFIER][TITLE]]'. While in Markdown they
;; are expressed as `[TITLE](denote:IDENTIFIER)'.
;;
;; When `denote-link' is called with a prefix argument (`C-u' by default),
;; it formats links like `[[denote:IDENTIFIER]]'. The user might prefer
;; its simplicity.
;;
;; Inserted links are automatically buttonized and remain active for as
;; long as the buffer is available. In Org this is handled by the major
;; mode: the `denote:' hyperlink type works exactly like the standard
;; `file:'. In Markdown and plain text, Denote performs the buttonization
;; of those links. To buttonize links in existing files while visiting
;; them, the user must add this snippet to their setup (it already excludes
;; Org):
;;
;; (add-hook 'find-file-hook #'denote-link-buttonize-buffer)
;;
;; Denote has a major-mode-agnostic mechanism to collect all linked file
;; references in the current buffer and return them as an appropriately
;; formatted list. This list can then be used in interactive commands.
;; The `denote-link-find-file' is such a command. It uses minibuffer
;; completion to visit a file that is linked to from the current note.
;; The candidates have the correct metadata, which is ideal for
;; integration with other standards-compliant tools (see manual's
;; "Extending Denote"). For instance, a package such as `marginalia'
;; will display accurate annotations, while the `embark' package will be
;; able to work its magic such as in exporting the list into a filtered
;; Dired buffer (i.e. a familiar Dired listing with only the files of
;; the current minibuffer session).
;;
;; The command `denote-link-backlinks' produces a bespoke buffer which
;; displays the file name of all notes linking to the current one. Each
;; file name appears on its own line and is buttonized so that it performs
;; the action of visiting the referenced file. The backlinks' buffer looks
;; like this:
;;
;; Backlinks to "On being honest" (20220614T130812)
;; ------------------------------------------------
;;
;; 20220614T145606--let-this-glance-become-a-stare__journal.txt
;; 20220616T182958--not-feeling-butterflies-in-your-stomach__journal.txt
;;
;; The backlinks' buffer is fontified by default, though the user has
;; access to the `denote-link-fontify-backlinks' option to disable this
;; effect by setting its value to nil.
;;
;; The placement of the backlinks' buffer is subject to the user option
;; `denote-link-backlinks-display-buffer-action'. Due to the nature of the
;; underlying `display-buffer' mechanism, this inevitably is a relatively
;; advanced feature. By default, the backlinks' buffer is displayed below
;; the current window. The doc string of our user option includes a sample
;; configuration that places the buffer in a left side window instead.
;; Reproducing it here for the sake of convenience:
;;
;; (setq denote-link-backlinks-display-buffer-action
;; '((display-buffer-reuse-window
;; display-buffer-in-side-window)
;; (side . left)
;; (slot . 99)
;; (window-width . 0.3)))
;;
;; The command `denote-link-add-links' adds links at point matching a
;; regular expression or plain string. The links are inserted as a
;; typographic list, such as:
;;
;; - link1
;; - link2
;; - link3
;;
;; Each link is formatted according to the file type of the current note,
;; as explained further above about the `denote-link' command. The current
;; note is excluded from the matching entries (adding a link to itself is
;; pointless).
;;
;; When called with a prefix argument (`C-u') `denote-link-add-links' will
;; format all links as `[[denote:IDENTIFIER]]', hence a typographic list:
;;
;; - [[denote:IDENTIFIER-1]]
;; - [[denote:IDENTIFIER-2]]
;; - [[denote:IDENTIFIER-3]]
;;
;; Same examples of a regular expression that can be used with this
;; command:
;;
;; - `journal' match all files which include `journal' anywhere in their
;; name.
;;
;; - `_journal' match all files which include `journal' as a keyword.
;;
;; - `^2022.*_journal' match all file names starting with `2022' and
;; including the keyword `journal'.
;;
;; - `\.txt' match all files including `.txt'. In practical terms, this
;; only applies to the file extension, as Denote automatically removes
;; dots (and other characters) from the base file name.
;;
;; If files are created with `denote-sort-keywords' as non-nil (the
;; default), then it is easy to write a regexp that includes multiple
;; keywords in alphabetic order:
;;
;; - `_denote.*_package' match all files that include both the `denote' and
;; `package' keywords, in this order.
;;
;; - `\(.*denote.*package.*\)\|\(.*package.*denote.*\)' is the same as
;; above, but out-of-order.
;;
;; Remember that regexp constructs only need to be escaped once (like `\|')
;; when done interactively but twice when called from Lisp. What we show
;; above is for interactive usage.
;;
;; For convenience, the `denote-link' command has an alias called
;; `denote-link-insert-link'. The `denote-link-backlinks' can also be used
;; as `denote-link-show-backlinks-buffer'. While `denote-link-add-links'
;; is aliased `denote-link-insert-links-matching-regexp'. The purpose of
;; these aliases is to offer alternative, more descriptive names of select
;; commands.
;;; Code:
(require 'denote-retrieve)
(defgroup denote-link ()
"Link facility for Denote."
:group 'denote)
;;;; User options
(defcustom denote-link-fontify-backlinks t
"When non-nil, apply faces to files in the backlinks' buffer."
:type 'boolean
:group 'denote-link)
(defcustom denote-link-backlinks-display-buffer-action
'((display-buffer-reuse-window display-buffer-below-selected)
(window-height . fit-window-to-buffer))
"The action used to display the current file's backlinks buffer.
The value has the form (FUNCTION . ALIST), where FUNCTION is
either an \"action function\", a list thereof, or possibly an
empty list. ALIST is a list of \"action alist\" which may be
omitted (or be empty).
Sample configuration to display the buffer in a side window on
the left of the Emacs frame:
(setq denote-link-backlinks-display-buffer-action
(quote ((display-buffer-reuse-window
display-buffer-in-side-window)
(side . left)
(slot . 99)
(window-width . 0.3))))
See Info node `(elisp) Displaying Buffers' for more details
and/or the documentation string of `display-buffer'."
:type '(cons (choice (function :tag "Display Function")
(repeat :tag "Display Functions" function))
alist)
:group 'denote-link)
;;;; Link to note
;; Arguments are: FILE-ID FILE-TITLE
(defconst denote-link--format-org "[[denote:%s][%s]]"
"Format of Org link to note.")
(defconst denote-link--format-markdown "[%2$s](denote:%1$s)"
"Format of Markdown link to note.")
(defconst denote-link--format-id-only "[[denote:%s]]"
"Format of identifier-only link to note.")
(defconst denote-link--regexp-org
(concat "\\[\\[" "denote:" "\\(?1:" denote--id-regexp "\\)" "]" "\\[.*?]]"))
(defconst denote-link--regexp-markdown
(concat "\\[.*?]" "(denote:" "\\(?1:" denote--id-regexp "\\)" ")"))
(defconst denote-link--regexp-plain
(concat "\\[\\[" "denote:" "\\(?1:" denote--id-regexp "\\)" "]]"))
(defun denote-link--file-type-format (current-file id-only)
"Return link format based on CURRENT-FILE format.
With non-nil ID-ONLY, use the generic link format without a
title."
;; Includes backup files. Maybe we can remove them?
(let ((current-file-ext (file-name-extension current-file)))
(cond
(id-only denote-link--format-id-only)
((string= current-file-ext "md")
denote-link--format-markdown)
;; Plain text also uses [[denote:ID][TITLE]]
(t denote-link--format-org))))
(defun denote-link--file-type-regexp (file)
"Return link regexp based on FILE format."
(pcase (file-name-extension file)
("md" denote-link--regexp-markdown)
(_ denote-link--regexp-org)))
(defun denote-link--format-link (file pattern)
"Prepare link to FILE using PATTERN."
(let ((file-id (denote-retrieve--filename-identifier file))
(file-title (unless (string= pattern denote-link--format-id-only)
(denote-retrieve--value-title file))))
(format pattern file-id file-title)))
;;;###autoload
(defun denote-link (target &optional id-only)
"Create link to TARGET note in variable `denote-directory'.
With optional ID-ONLY, such as a universal prefix
argument (\\[universal-argument]), insert links with just the
identifier and no further description. In this case, the link
format is always [[denote:IDENTIFIER]]."
(interactive (list (denote-retrieve--read-file-prompt) current-prefix-arg))
(let ((beg (point)))
(insert
(denote-link--format-link
target
(denote-link--file-type-format (buffer-file-name) id-only)))
(unless (derived-mode-p 'org-mode)
(make-button beg (point) 'type 'denote-link-button))))
(defalias 'denote-link-insert-link (symbol-function 'denote-link))
(defun denote-link--collect-identifiers (regexp)
"Return collection of identifiers in buffer matching REGEXP."
(let (matches)
(save-excursion
(goto-char (point-min))
(while (re-search-forward regexp nil t)
(push (match-string-no-properties 1) matches)))
matches))
(defun denote-link--expand-identifiers (regexp)
"Expend identifiers matching REGEXP into file paths."
(let ((files (denote--directory-files))
(found-files))
(dolist (file files)
(dolist (i (denote-link--collect-identifiers regexp))
(when (string-prefix-p i (file-name-nondirectory file))
(push file found-files))))
found-files))
(defvar denote-link--find-file-history nil
"History for `denote-link-find-file'.")
(defun denote-link--find-file-prompt (files)
"Prompt for linked file among FILES."
(completing-read "Find linked file "
(denote--completion-table 'file files)
nil t
nil 'denote-link--find-file-history))
;; TODO 2022-06-14: Do we need to add any sort of extension to better
;; integrate with Embark? For the minibuffer interaction it is not
;; necessary, but maybe it can be done to immediately recognise the
;; identifiers are links to files?
;;;###autoload
(defun denote-link-find-file ()
"Use minibuffer completion to visit linked file."
(interactive)
(if-let* ((regexp (denote-link--file-type-regexp (buffer-file-name)))
(files (denote-link--expand-identifiers regexp)))
(find-file (denote-link--find-file-prompt files))
(user-error "No links found in the current buffer")))
;;;; Link buttons
;; Evaluate: (info "(elisp) Button Properties")
;;
;; Button can provide a help-echo function as well, but I think we might
;; not need it.
(define-button-type 'denote-link-button
'follow-link t
'action #'denote-link--find-file-at-button)
(autoload 'thing-at-point-looking-at "thingatpt")
(defun denote-link--link-at-point-string ()
"Return identifier at point."
(when (or (thing-at-point-looking-at denote-link--regexp-plain)
(thing-at-point-looking-at denote-link--regexp-markdown)
(thing-at-point-looking-at denote-link--regexp-org)
;; Meant to handle the case where a link is broken by
;; `fill-paragraph' into two lines, in which case it
;; buttonizes only the "denote:ID" part. Example:
;;
;; [[denote:20220619T175212][This is a
;; test]]
;;
;; Maybe there is a better way?
(thing-at-point-looking-at "\\[\\(denote:.*\\)]"))
(match-string-no-properties 0)))
(defun denote-link--id-from-string (string)
"Extract identifier from STRING."
(replace-regexp-in-string
(concat ".*denote:" "\\(" denote--id-regexp "\\)" ".*")
"\\1" string))
;; NOTE 2022-06-15: I add this as a variable for advanced users who may
;; prefer something else. If there is demand for it, we can make it a
;; defcustom, but I think it would be premature at this stage.
(defvar denote-link-buton-action #'find-file-other-window
"Action for Denote buttons.")
(defun denote-link--find-file-at-button (button)
"Visit file referenced by BUTTON."
(let* ((id (denote-link--id-from-string
(buffer-substring-no-properties
(button-start button)
(button-end button))))
(file (denote--get-note-path-by-id id)))
(funcall denote-link-buton-action file)))
;;;###autoload
(defun denote-link-buttonize-buffer (&optional beg end)
"Make denote: links actionable buttons in the current buffer.
Add this to `find-file-hook'. It will only work with Denote
notes and will not do anything in `org-mode' buffers, as buttons
already work there. If you do not use Markdown or plain text,
then you do not need this.
When called from Lisp, with optional BEG and END as buffer
positions, limit the process to the region in-between."
(when (and (not (derived-mode-p 'org-mode)) (denote--current-file-is-note-p))
(save-excursion
(goto-char (or beg (point-min)))
(while (re-search-forward denote--id-regexp end t)
(when-let ((string (denote-link--link-at-point-string))
(beg (match-beginning 0))
(end (match-end 0)))
(make-button beg end 'type 'denote-link-button))))))
;;;; Backlinks' buffer
(define-button-type 'denote-link-backlink-button
'follow-link t
'action #'denote-link--backlink-find-file
'face 'unspecified) ; we use this face attribute to style it later
(defun denote-link--backlink-find-file (button)
"Action for BUTTON to `find-file'."
(funcall denote-link-buton-action (buffer-substring (button-start button) (button-end button))))
(defun denote-link--display-buffer (buf)
"Run `display-buffer' on BUF.
Expand `denote-link-backlinks-display-buffer-action'."
(display-buffer
buf
`(,@denote-link-backlinks-display-buffer-action)))
(require 'denote-faces)
(defun denote-link--prepare-backlinks (id files &optional title)
"Create backlinks' buffer for ID including FILES.
Use optional TITLE for a prettier heading."
(let ((inhibit-read-only t)
(buf (format "*denote-backlinks to %s*" id)))
(with-current-buffer (get-buffer-create buf)
(erase-buffer)
(special-mode)
(goto-char (point-min))
(when-let* ((title)
(heading (format "Backlinks to %S (%s)" title id))
(l (length heading)))
(insert (format "%s\n%s\n\n" heading (make-string l ?-))))
(mapc (lambda (f)
(insert f)
(make-button (point-at-bol) (point-at-eol) :type 'denote-link-backlink-button)
(newline))
files)
(goto-char (point-min))
(when denote-link-fontify-backlinks
(font-lock-add-keywords nil denote-faces-file-name-with-subdir-keywords t)))
(denote-link--display-buffer buf)))
;;;###autoload
(defun denote-link-backlinks ()
"Produce a buffer with files linking to current note.
Each file is a clickable/actionable button that visits the
referenced entry. Files are fontified if the user option
`denote-link-fontify-backlinks' is non-nil.
The placement of the backlinks' buffer is controlled by the user
option `denote-link-backlinks-display-buffer-action'. By
default, it will show up below the current window."
(interactive)
(let* ((default-directory (denote-directory))
(file (buffer-file-name))
(id (denote-retrieve--filename-identifier file))
(title (denote-retrieve--value-title file)))
(if-let ((files (denote-retrieve--proces-grep id)))
(denote-link--prepare-backlinks id files title)
(user-error "No links to the current note"))))
(defalias 'denote-link-show-backlinks-buffer (symbol-function 'denote-link-backlinks))
;;;; Add links matching regexp
(defvar denote-link--links-to-files nil
"String of `denote-link-add-links-matching-keyword'.")
(defvar denote-link--prepare-links-format "- %s\n"
"Format specifiers for `denote-link-add-links'.")
;; NOTE 2022-06-16: There is no need to overwhelm the user with options,
;; though I expect someone to want to change the sort order.
(defvar denote-link-add-links-sort nil
"Add REVERSE to `sort-lines' of `denote-link-add-links' when t.")
(defun denote-link--prepare-links (files current-file id-only)
"Prepare links to FILES from CURRENT-FILE.
When ID-ONLY is non-nil, use a generic link format. See
`denote-link--file-type-format'."
(setq denote-link--links-to-files
(with-temp-buffer
(mapc (lambda (file)
(insert
(format
denote-link--prepare-links-format
(denote-link--format-link
file
(denote-link--file-type-format current-file id-only)))))
files)
(sort-lines denote-link-add-links-sort (point-min) (point-max))
(buffer-string))))
(defvar denote-link--add-links-history nil
"Minibuffer history for `denote-link-add-links'.")
;;;###autoload
(defun denote-link-add-links (regexp &optional id-only)
"Insert links to all notes matching REGEXP.
Use this command to reference multiple files at once.
Particularly useful for the creation of metanotes (read the
manual for more on the matter).
Optional ID-ONLY has the same meaning as in `denote-link': it
inserts links with just the identifier."
(interactive
(list
(read-regexp "Insert links matching REGEX: " nil 'denote-link--add-links-history)
current-prefix-arg))
(let* ((default-directory (denote-directory))
(current-file (buffer-file-name)))
(if-let ((files (denote--directory-files-matching-regexp regexp)))
(let ((beg (point)))
(insert (denote-link--prepare-links files current-file id-only))
(unless (derived-mode-p 'org-mode)
(denote-link-buttonize-buffer beg (point))))
(user-error "No links matching `%s'" regexp))))
(defalias 'denote-link-insert-links-matching-regexp (symbol-function 'denote-link-add-links))
;;;; Register `denote:' custom Org hyperlink
(autoload 'org-link-set-parameters "ol.el")
(org-link-set-parameters
"denote"
:follow #'denote-link-ol-follow
:complete #'denote-link-ol-complete
:export #'denote-link-ol-export)
(declare-function org-link-open-as-file "ol" (path arg))
(defun denote-link--ol-resolve-link-to-target (link &optional path-id)
"Resolve LINK into the appropriate target.
With optional PATH-ID return a cons cell consisting of the path
and the identifier."
(let* ((search (and (string-match "::\\(.*\\)\\'" link)
(match-string 1 link)))
(id (if (and (stringp search) (not (string-empty-p search)))
(substring link 0 (match-beginning 0))
link))
(path (denote--get-note-path-by-id id)))
(cond
(path-id
(cons (format "%s" path) (format "%s" id)))
((and (stringp search) (not (string-empty-p search)))
(concat path "::" search))
(path))))
(defun denote-link-ol-follow (link)
"Find file of type `denote:' matching LINK.
LINK is the identifier of the note, optionally followed by a
search option akin to that of standard Org `file:' link types.
Read Info node `(org) Search Options'.
Uses the function `denote-directory' to establish the path to the
file."
(org-link-open-as-file
(denote-link--ol-resolve-link-to-target link)
nil))
(defun denote-link-ol-complete ()
"Like `denote-link' but for Org integration.
This lets the user complete a link through the `org-insert-link'
interface by first selecting the `denote:' hyperlink type."
(concat
"denote:"
(denote-retrieve--filename-identifier (denote-retrieve--read-file-prompt))))
(defun denote-link-ol-export (link description format)
"Export a `denote:' link from Org files.
The LINK, DESCRIPTION, and FORMAT are handled by the export
backend."
(let* ((path-id (denote-link--ol-resolve-link-to-target link :path-id))
(path (file-name-nondirectory (car path-id)))
(p (file-name-sans-extension path))
(id (cdr path-id))
(desc (or description (concat "denote:" id))))
(cond
((eq format 'html) (format "<a target=\"_blank\" href=\"%s.html\">%s</a>" p desc))
((eq format 'latex) (format "\\href{%s}{%s}" (replace-regexp-in-string "[\\{}$%&_#~^]" "\\\\\\&" path) desc))
((eq format 'texinfo) (format "@uref{%s,%s}" path desc))
((eq format 'ascii) (format "[%s] <denote:%s>" desc path)) ; NOTE 2022-06-16: May be tweaked further
((eq format 'md) (format "[%s](%s.md)" desc p))
(t path))))
(provide 'denote-link)
;;; denote-link.el ends here

106
denote-org-capture.el Normal file
View file

@ -0,0 +1,106 @@
;;; denote-org-capture.el --- Integration between Denote and org-capture -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
;; URL: https://git.sr.ht/~protesilaos/denote
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
;; Version: 0.2.1
;; Package-Requires: ((emacs "27.2"))
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Denote integration with org-capture.
;;
;; Samples of an `org-capture-templates' entry:
;;
;; (setq org-capture-templates
;; '(("n" "New note (with denote.el)" plain
;; (file denote-last-path)
;; #'denote-org-capture
;; :no-save t
;; :immediate-finish nil
;; :kill-buffer t
;; :jump-to-captured t)))
;;
;; (with-eval-after-load 'org-capture
;; (add-to-list 'org-capture-templates
;; '("n" "New note (with denote.el)" plain
;; (file denote-last-path)
;; #'denote-org-capture
;; :no-save t
;; :immediate-finish nil
;; :kill-buffer t
;; :jump-to-captured t)))
;;
;; Note that `denote-org-capture' ignores the `denote-file-type': it
;; always sets the Org file extension for the created note to ensure
;; that the capture process works as intended, especially for the
;; desired output of the `denote-org-capture-specifiers'.
;;; Code:
(require 'denote)
(defgroup denote-org-capture ()
"Integration between Denote and Org Capture."
:group 'denote)
(defcustom denote-org-capture-specifiers "%l\n%i\n%?"
"String with format specifiers for `org-capture-templates'.
Check that variable's documentation for the details.
This string is appended to new notes in the `denote-org-capture'
function. Every new note has the standard front matter we
define."
:type 'string
:group 'denote-org-capture)
;;;###autoload
(defun denote-org-capture ()
"Create new note through `org-capture-templates'.
Use this as a function that returns the path to the new file.
The file is populated with Denote's front matter. It can then be
expanded with the usual specifiers or strings that
`org-capture-templates' supports.
Note that this function ignores the `denote-file-type': it always
sets the Org file extension for the created note to ensure that
the capture process works as intended, especially for the desired
output of the `denote-org-capture-specifiers'.
Consult the manual for template samples."
(let ((title (denote--title-prompt))
(keywords (denote--keywords-prompt))
(denote-file-type nil)) ; we enforce the .org extension for `org-capture'
(denote--path title keywords)
(denote--prepare-note denote-last-title denote-last-keywords denote-last-path)
(denote--keywords-add-to-history denote-last-keywords)
(concat denote-last-front-matter denote-org-capture-specifiers)))
(defun denote-org-capture-delete-empty-file ()
"Delete file if capture with `denote-org-capture' is aborted."
(when-let* ((file denote-last-path)
((denote--file-empty-p file)))
(delete-file denote-last-path)))
(add-hook 'org-capture-after-finalize-hook #'denote-org-capture-delete-empty-file)
(provide 'denote-org-capture)
;;; denote-org-capture.el ends here

118
denote-retrieve.el Normal file
View file

@ -0,0 +1,118 @@
;;; denote-retrieve.el --- Internal search functions for Denote -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
;; URL: https://git.sr.ht/~protesilaos/denote
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
;; Version: 0.2.1
;; Package-Requires: ((emacs "27.2"))
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;;
;; Used internally by commands that operate on file contents.
;;; Code:
(require 'denote)
(require 'xref)
(defconst denote-retrieve--title-front-matter-key-regexp
"^\\(?:#\\+\\)?\\(?:title\\)\\s-*[:=]"
"Regular expression for title key.")
(defconst denote-retrieve--id-front-matter-key-regexp
"^.?.?\\b\\(?:identifier\\)\\s-*[:=]"
"Regular expression for identifier key.")
(defconst denote-retrieve--date-front-matter-key-regexp
"^\\(?:#\\+\\)?\\(?:date\\)\\s-*[:=]"
"Regular expression for date key.")
(defun denote-retrieve--filename-identifier (file)
"Extract identifier from FILE name."
(if (file-exists-p file)
(progn
(string-match denote--id-regexp file)
(match-string 0 file))
(error "Cannot find `%s' as a file" file)))
(defun denote-retrieve--search (file key-regexp &optional key)
"Return value of KEY-REGEXP key in current buffer from FILE.
If optional KEY is non-nil, return the key instead."
(with-temp-buffer
(insert-file-contents file)
(save-excursion
(save-restriction
(widen)
(goto-char (point-min))
(when (re-search-forward key-regexp nil t 1)
(if key
(match-string-no-properties 0)
(let ((trims "[ \t\n\r\"']+"))
(string-trim
(buffer-substring-no-properties (point) (point-at-eol))
trims trims))))))))
(defun denote-retrieve--value-title (file &optional key)
"Return title value from FILE.
If optional KEY is non-nil, return the key instead."
(denote-retrieve--search file denote-retrieve--title-front-matter-key-regexp key))
(defun denote-retrieve--value-date (file &optional key)
"Return date value from FILE.
If optional KEY is non-nil, return the key instead."
(denote-retrieve--search file denote-retrieve--date-front-matter-key-regexp key))
(defun denote-retrieve--read-file-prompt ()
"Prompt for regular file in variable `denote-directory'."
(read-file-name "Select note: " (denote-directory) nil nil nil
(lambda (f) (or (denote--only-note-p f) (file-directory-p f)))))
(defun denote-retrieve--files-in-output (files)
"Return list of FILES from `find' output."
(delq nil (mapcar (lambda (f)
(when (denote--only-note-p f) f))
files)))
(defun denote-retrieve--xrefs (identifier)
"Return xrefs of IDENTIFIER in variable `denote-directory'.
The xrefs are returned as an alist."
(xref--analyze
(xref-matches-in-files identifier (denote--directory-files :absolute))))
(defun denote-retrieve--files-in-xrefs (xrefs)
"Return sorted file names sans directory from XREFS.
Parse `denote-retrieve--xrefs'."
(sort
(mapcar (lambda (x)
(denote--file-name-relative-to-denote-directory (car x)))
xrefs)
#'string-lessp))
(defun denote-retrieve--proces-grep (identifier)
"Process lines matching IDENTIFIER and return list of files."
(let* ((default-directory (denote-directory))
(file (denote--file-name-relative-to-denote-directory (buffer-file-name))))
(denote-retrieve--files-in-output
(delete file (denote-retrieve--files-in-xrefs
(denote-retrieve--xrefs identifier))))))
(provide 'denote-retrieve)
;;; denote-retrieve.el ends here

7785
denote.el

File diff suppressed because it is too large Load diff

View file

@ -1,483 +0,0 @@
;;; denote-test.el --- Unit tests for Denote -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Free Software Foundation, Inc.
;; Author: Protesilaos <info@protesilaos.com>
;; Maintainer: Protesilaos <info@protesilaos.com>
;; URL: https://github.com/protesilaos/denote
;; 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Tests for Denote. Note that we are using Shorthands in this file,
;; so the "dt-" prefix really is "denote-test-". Evaluate the
;; following to learn more:
;;
;; (info "(elisp) Shorthands")
;;; Code:
(require 'ert)
;;;; Tests for denote.el
(require 'denote)
(ert-deftest dt-denote-directories--get-paths ()
"Test that `denote-directories--get-paths' returns a list of paths."
(let ((path "~/Documents/notes"))
(should (equal
(denote-directories--get-paths path)
(list (file-name-as-directory (expand-file-name path))))))
(let ((paths (list "~/Documents/notes" "~/Documents/books")))
(should (equal
(denote-directories--get-paths paths)
(mapcar
(lambda (path)
(file-name-as-directory (expand-file-name path)))
paths)))
(should (seq-every-p
(lambda (path)
(string-suffix-p "/" path))
(denote-directories--get-paths paths)))
(should-not (null (denote-directories--get-paths paths))))
(should-error (denote-directories--get-paths 'hello))
(should-error (denote-directories--get-paths '(hello)))
(should-error (denote-directories--get-paths '(1 2 3))))
(ert-deftest dt-denote--make-denote-directory ()
"Test that `denote--make-denote-directory' creates the directory."
(let* ((names (list "denote-test-made-directory-1" "denote-test-made-directory-2"))
(directories (mapcar
(lambda (directory)
(file-name-as-directory (expand-file-name directory temporary-file-directory)))
names)))
(should-not (denote-directories--make-paths directories))
(should (seq-every-p #'file-name-directory directories))
(dolist (directory directories)
(delete-directory directory))))
(ert-deftest dt-denote-directories ()
"Test that `denote-directories' returns list of directories.
It does so by expanding the value of the user option `denote-directory'.
Also see `denote-test--denote--make-denote-directory',
`denote-test---denote-directories--get-paths'."
(let ((denote-directory "/tmp/denote-test-notes"))
(should
(equal
(denote-directories)
(list (file-name-as-directory (expand-file-name denote-directory)))))
(should (and (file-directory-p denote-directory)
(file-name-absolute-p denote-directory))))
(let ((denote-directory (list "/tmp/denote-test-notes-1" "/tmp/denote-test-notes-2")))
(should
(equal
(denote-directories)
(mapcar
(lambda (directory)
(file-name-as-directory (expand-file-name directory)))
denote-directory)))
(should (and (seq-every-p #'file-directory-p denote-directory)
(seq-every-p #'file-name-absolute-p denote-directory)))))
(ert-deftest dt-denote-sluggify-title ()
"Test that `denote-sluggify-title' removes punctuation from the string.
Concretely, remove anything specified in `denote-sluggify-title'."
(should (equal (denote-sluggify-title "this-is-!@#test") "this-is-test")))
(ert-deftest dt-denote-slug-keep-only-ascii ()
"Test that `denote-slug-keep-only-ascii' removes non-ASCII characters."
(should (equal
(denote-slug-keep-only-ascii "There are no-ASCII characters here 😀")
"There are no-ASCII characters here ")))
(ert-deftest dt-denote-slug-hyphenate ()
"Test that `denote-slug-hyphenate' hyphenates the string.
Also replace multiple hyphens with a single one and remove any
leading and trailing hyphen."
(should (equal (denote-slug-hyphenate "__ This is a test __ ") "This-is-a-test")))
(ert-deftest dt-denote-sluggify ()
"Test that `denote-sluggify' sluggifies the string.
To sluggify is to (i) downcase, (ii) hyphenate, (iii) de-punctuate, and (iv) remove spaces from the string."
(should (equal (denote-sluggify 'title " ___ !~!!$%^ This iS a tEsT ++ ?? ") "this-is-a-test")))
(ert-deftest Ddenote--slug-put-equals ()
"Test that `denote-slug-put-equals' replaces spaces/underscores with =.
Otherwise do the same as what is described in
`denote-test--denote-slug-hyphenate'.
The use of the equals sign is for the SIGNATURE field of the
Denote file name."
(should (equal (denote-slug-put-equals "__ This is a test __ ") "This=is=a=test")))
(ert-deftest dt-denote-sluggify-signature ()
"Test that `denote-sluggify-signature' sluggifies the string for file signatures.
This is like `denote-test--denote-sluggify', except that it also
accounts for what we describe in `denote-test--denote-slug-put-equals'."
(should (equal (denote-sluggify-signature "--- ___ !~!!$%^ This -iS- a tEsT ++ ?? ") "this=is=a=test")))
(ert-deftest dt-denote-sluggify-keyword ()
"Test that `denote-sluggify-keyword' sluggifies the string while joining words.
In this context, to join words is to elimitate any space or
delimiter between them.
Otherwise, this is like `denote-test--denote-sluggify'."
(should (equal (denote-sluggify-keyword "--- ___ !~!!$%^ This iS a - tEsT ++ ?? ") "thisisatest")))
(ert-deftest dt-denote-sluggify-keywords ()
"Test that `denote-sluggify-keywords' sluggifies a list of strings.
The function also account for the value of the user option
`denote-allow-multi-word-keywords'."
(should (equal (denote-sluggify-keywords '("one !@# --- one" " two" "__ three __")) '("oneone" "two" "three"))))
(ert-deftest dt-denote-slug-remove-accents ()
"Test that `denote-slug-remove-accents' removed diacretics."
(should (string= (denote-slug-remove-accents "ê") "e"))
(should (string= (denote-slug-remove-accents "ñ") "n")))
(ert-deftest dt-denote--file-empty-p ()
"Test that `denote--file-empty-p' returns non-nil on empty file."
(let ((file (make-temp-file "denote-test")))
(should (denote--file-empty-p file))
(with-current-buffer (find-file-noselect file)
(insert "Hello world, this is not empty anymore!")
(save-buffer))
(should-not (denote--file-empty-p file))
(delete-file file)))
(ert-deftest dt-denote-file-has-denoted-filename-p ()
"Test that `denote-file-has-denoted-filename-p' validates a Denote file name."
(should (denote-file-has-denoted-filename-p "20230522T154900--test__keyword.txt"))
(should-not (denote-file-has-denoted-filename-p "hello")))
(ert-deftest dt-denote-file-has-identifier-p ()
"Test that `denote-file-has-identifier-p' checks for a Denote identifier."
(should (denote-file-has-identifier-p "20230522T154900--test__keyword.txt"))
(should-not (denote-file-has-identifier-p "T154900--test__keyword.txt")))
(ert-deftest dt-denote-file-has-signature-p ()
"Test that `denote-file-has-signature-p' checks for a Denote signature."
(should (denote-file-has-signature-p "20230522T154900==sig--test__keyword.txt"))
(should-not (denote-file-has-signature-p "20230522T154900--test__keyword.txt")))
(ert-deftest dt-denote-file-has-supported-extension-p ()
"Test that `denote-file-has-supported-extension-p' matches a supported extension."
(let ((extensions (denote-file-type-extensions-with-encryption)))
(should
(member
(file-name-extension "20230522T154900==sig--test__keyword.txt" :period)
extensions))
(should-not
(member
(file-name-extension "20230522T154900==sig--test__keyword" :period)
extensions))))
(ert-deftest dt-denote-file-type-extensions ()
"Test that `denote-file-type-extensions' returns file extensions.
We check for the common file type extensions, though the user can set
`denote-file-types' to nil and handle things on their own or, anyhow,
modify that variable."
(let ((extensions (denote-file-type-extensions)))
(should (or (member ".md" extensions)
(member ".org" extensions)
(member ".txt" extensions)))))
(ert-deftest dt-denote-file-type-extensions-with-encryption ()
"Test that `denote-file-type-extensions-with-encryption' covers encryption.
Extend what we do in `denote-test--denote-file-type-extensions'."
(let ((extensions (denote-file-type-extensions-with-encryption)))
(should (or (member ".md" extensions)
(member ".org" extensions)
(member ".txt" extensions)
(member ".md.gpg" extensions)
(member ".org.gpg" extensions)
(member ".txt.gpg" extensions)
(member ".md.age" extensions)
(member ".org.age" extensions)
(member ".txt.age" extensions)))))
(ert-deftest dt-denote--format-front-matter ()
"Test that `denote--format-front-matter' formats front matter correctly.
To make the test reproducible, set `denote-date-format' to a value that
does not involve the time zone."
(let ((denote-date-format "%Y-%m-%d")
(denote-front-matter-components-present-even-if-empty-value '(title keywords signature date identifier)))
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") '("") "" "" 'text)
(string-join
'("title: "
"date: 2024-01-01"
"tags: "
"identifier: "
"signature: "
"---------------------------\n\n")
"\n")))
(should (equal (denote--format-front-matter "Some test" (date-to-time "2023-06-05") '("one" "two") "20230605T102234" "sig" 'text)
(string-join
'("title: Some test"
"date: 2023-06-05"
"tags: one two"
"identifier: 20230605T102234"
"signature: sig"
"---------------------------\n\n")
"\n")))
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") nil "" "" 'org)
(string-join
'("#+title: "
"#+date: 2024-01-01"
"#+filetags: "
"#+identifier: "
"#+signature: "
"\n")
"\n")))
(should (equal
(denote--format-front-matter
"Some test" (date-to-time "2023-06-05") '("one" "two")
"20230605T102234" "sig" 'org)
(string-join
'("#+title: Some test"
"#+date: 2023-06-05"
"#+filetags: :one:two:"
"#+identifier: 20230605T102234"
"#+signature: sig"
"\n")
"\n")))
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") nil "" "" 'markdown-yaml)
(string-join
'("---"
"title: \"\""
"date: 2024-01-01"
"tags: []"
"identifier: \"\""
"signature: \"\""
"---"
"\n")
"\n")))
(should (equal (denote--format-front-matter "Some test" (date-to-time "2023-06-05") '("one" "two") "20230605T102234" "sig" 'markdown-yaml)
(string-join
'("---"
"title: \"Some test\""
"date: 2023-06-05"
"tags: [\"one\", \"two\"]"
"identifier: \"20230605T102234\""
"signature: \"sig\""
"---"
"\n")
"\n")))
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") nil "" "" 'markdown-toml)
(string-join
'("+++"
"title = \"\""
"date = 2024-01-01"
"tags = []"
"identifier = \"\""
"signature = \"\""
"+++"
"\n")
"\n")))
(should (equal (denote--format-front-matter "Some test" (date-to-time "2023-06-05") '("one" "two") "20230605T102234" "sig" 'markdown-toml)
(string-join
'("+++"
"title = \"Some test\""
"date = 2023-06-05"
"tags = [\"one\", \"two\"]"
"identifier = \"20230605T102234\""
"signature = \"sig\""
"+++"
"\n")
"\n")))))
(ert-deftest dt-denote-format-file-name ()
"Test that `denote-format-file-name' returns all expected paths."
(let* ((title "Some test")
(id (format-time-string denote-date-identifier-format (denote-valid-date-p "2023-11-28 05:53:11")))
(denote-directory "/tmp/test-denote")
(dir (car (denote-directories)))
(ext (denote--file-extension 'org))
(kws '("one" "two")))
(should-error (denote-format-file-name nil id kws title ext ""))
(should-error (denote-format-file-name "" id kws title ext ""))
;; NOTE that `denote-directory' is the `let' bound value without the suffix
(should-error (denote-format-file-name denote-directory id kws title ext ""))
(should-error (denote-format-file-name dir "" nil "" ext ""))
(should (equal (denote-format-file-name dir nil kws title ext "") "/tmp/test-denote/--some-test__one_two.org"))
(should (equal (denote-format-file-name dir "" kws title ext "") "/tmp/test-denote/--some-test__one_two.org"))
(should (equal (denote-format-file-name dir "0123456" kws title ext "") "/tmp/test-denote/@@0123456--some-test__one_two.org"))
(should (equal (denote-format-file-name dir id kws title ext "") "/tmp/test-denote/20231128T055311--some-test__one_two.org"))
(should (equal (denote-format-file-name dir id nil "" ext "") "/tmp/test-denote/20231128T055311.org"))
(should (equal (denote-format-file-name dir id nil nil ext nil) "/tmp/test-denote/20231128T055311.org"))
(should (equal (denote-format-file-name dir id kws title ext "sig") "/tmp/test-denote/20231128T055311==sig--some-test__one_two.org"))))
(ert-deftest dt-denote-get-file-extension ()
"Test that `denote-get-file-extension' gets the correct file extension."
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing") ""))
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org") ".org"))
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org.gpg") ".org.gpg"))
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org.age") ".org.age")))
(ert-deftest dt-denote-get-file-extension-sans-encryption ()
"Test that `denote-get-file-extension-sans-encryption' gets the file extension without encryption."
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing") ""))
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org") ".org"))
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org.gpg") ".org"))
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org.age") ".org")))
;; TODO 2026-04-06: For the `denote-file-type'.
(ert-deftest dt-denote-filetype-heuristics ()
"Test that `denote-filetype-heuristics' gets the correct file type."
(should-not (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing"))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org") 'org))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org.gpg") 'org))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org.age") 'org))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt") 'text))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt.gpg") 'text))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt.age") 'text))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md")
(caar (denote--file-types-with-extension ".md"))))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md.gpg")
(caar (denote--file-types-with-extension ".md"))))
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md.age")
(caar (denote--file-types-with-extension ".md")))))
(ert-deftest dt-denote-retrieve-filename-identifier ()
"Test that `denote-retrieve-filename-identifier' returns only the identifier."
(should (null (denote-retrieve-filename-identifier "/path/to/testing/--this-is-a-test-reordered__denote_testing.org")))
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org") "20240610T194654"))
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "20240610T194654"))
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org") "20240610T194654"))
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org") "20240610T194654"))
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "20240610T194654"))
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "20240610T194654")))
(ert-deftest dt-denote-retrieve-filename-title ()
"Test that `denote-retrieve-filename-title' returns only the title."
(should (null (denote-retrieve-filename-title "/path/to/testing/20240610T194654__denote_testing.org")))
(should (equal (denote-retrieve-filename-title "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org") "this-is-a-test-reordered"))
(should (equal (denote-retrieve-filename-title "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "this-is-a-test-reordered"))
(should (equal (denote-retrieve-filename-title "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org") "this-is-a-test-reordered"))
(should (equal (denote-retrieve-filename-title "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org") "this-is-a-test-reordered"))
(should (equal (denote-retrieve-filename-title "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "this-is-a-test-reordered"))
(should (equal (denote-retrieve-filename-title "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "this-is-a-test-reordered")))
(ert-deftest dt-denote-retrieve-filename-keywords ()
"Test that `denote-retrieve-filename-keywords' returns only the keywords."
(should (null (denote-retrieve-filename-keywords "/path/to/testing/20240610T194654--this-is-a-test-reordered.org")))
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org") "denote_testing"))
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "denote_testing"))
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org") "denote_testing"))
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org") "denote_testing"))
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "denote_testing"))
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "denote_testing")))
(ert-deftest dt-denote-retrieve-filename-signature ()
"Test that `denote-retrieve-filename-signature' returns only the signature."
(should (null (denote-retrieve-filename-signature "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org")))
(should (equal (denote-retrieve-filename-signature "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "signature"))
(should (equal (denote-retrieve-filename-signature "/path/to/testing/--this-is-a-test-reordered==signature__denote_testing@@20240610T194654.org") "signature"))
(should (equal (denote-retrieve-filename-signature "/path/to/testing/__denote_testing--this-is-a-test-reordered==signature@@20240610T194654.org") "signature"))
(should (equal (denote-retrieve-filename-signature "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered==signature.org") "signature"))
(should (equal (denote-retrieve-filename-signature "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "signature")))
(ert-deftest dt-denote-date-identifier-p ()
"Test that `denote-date-identifier-p' works for Denote identifiers."
(should (denote-date-identifier-p "20240901T090910"))
(should-not (denote-date-identifier-p "20240901T090910-not-identifier-format")))
(ert-deftest dt-denote-id-to-date ()
"Test that `denote-id-to-date' returns the date from an identifier."
(should (equal (denote-id-to-date "20240901T090910") "2024-09-01"))
(should-error (denote-id-to-date "20240901T090910-not-identifier-format")))
(ert-deftest dt-denote--date-convert ()
"Test that `denote--date-convert' works with dates."
(should (equal (denote--date-convert '(26454 45206 461174 657000) :list) '(26454 45206 461174 657000)))
(should (equal (denote--date-convert '(26454 45206 461174 657000) :string) "2024-12-09 10:55:50"))
(should (equal (denote--date-convert nil :string) ""))
(should (equal (denote--date-convert nil :list) nil))
(should-error (denote--date-convert '(26454 45206 461174 657000) :not-valid-type))
(should-error (denote--date-convert nil :not-valid-type)))
;; TODO 2026-02-23: Add support for Windows paths. But I need somebody who has a Windows machine...
(ert-deftest dt-denote--get-common-root-directory ()
"Test that `denote--get-common-root-directory' returns the right path."
(should (string=
(denote--get-common-root-directory
'("/home/prot/Documents/notes/"
"/home/prot/Documents/notes/attachments"))
"/home/prot/Documents/notes/"))
(should (string=
(denote--get-common-root-directory
'("/home/prot/Documents/"
"/home/prot/Documents/notes/attachments"))
"/home/prot/Documents/"))
(should (string=
(denote--get-common-root-directory
'("/home/prot/Books/"
"/home/prot/Documents/notes/"))
"/home/prot/"))
(should (string=
(denote--get-common-root-directory
'("/tmp/notes/"
"/home/prot/Documents/notes/"))
"/"))
(should (string=
(denote--get-common-root-directory
'("/home/prot/Documents/notes/"))
"/home/prot/Documents/notes/"))
(should-error (denote--get-common-root-directory '("~/Documents/notes/"))))
(ert-deftest dt-denote-query--keywords-as-regexp ()
"Test that `denote-query--keywords-as-regexp' works as intended."
(should (string= (denote-query--keywords-as-regexp '("one" "two")) "_\\(?:one\\|two\\)"))
(should-error (denote-query--keywords-as-regexp "one"))
(should-error (denote-query--keywords-as-regexp '("one" 1))))
(ert-deftest dt-denote-file-type ()
"Test that function `denote-file-type' does what it is meant to."
(let* ((create-file-fn (lambda (name extension)
(expand-file-name
(format "denote-test-file-type-%s.%s" name extension)
(temporary-file-directory))))
(modify-file-fn (lambda (file contents)
(with-current-buffer (find-file-noselect file)
(erase-buffer)
(insert contents)
(save-buffer))))
(yaml-file (funcall create-file-fn "yaml" "md"))
(toml-file (funcall create-file-fn "toml" "md"))
(org-file (funcall create-file-fn "org" "org")))
(progn
(funcall modify-file-fn yaml-file denote-yaml-front-matter)
(should (eq (denote-file-type yaml-file) 'markdown-yaml)))
(progn
(funcall modify-file-fn toml-file denote-toml-front-matter)
(should (eq (denote-file-type toml-file) 'markdown-toml)))
(progn
(funcall modify-file-fn org-file denote-org-front-matter)
(should (eq (denote-file-type org-file) 'org)))
(should-not (denote-file-type "test"))
(let ((denote-file-types nil))
(should-not (denote-file-type "test.md")))))
(provide 'denote-test)
;;; denote-test.el ends here
;; Local Variables:
;; read-symbol-shorthands: (("dt" . "denote-test-"))
;; End: