Compare commits

..

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

12 changed files with 5148 additions and 9780 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,10 +1,5 @@
# 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

4792
README.org

File diff suppressed because it is too large Load diff

252
denote-journal-extras.el Normal file
View file

@ -0,0 +1,252 @@
;;; denote-journal-extras.el --- Convenience functions for daily journaling -*- lexical-binding: t; -*-
;; Copyright (C) 2023-2024 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Protesilaos Stavrou <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:
;; This is a set of optional convenience functions that used to be
;; provided in the Denote manual. They facilitate the use of Denote
;; for daily journaling.
;;; Code:
(require 'denote)
(defgroup denote-journal-extras nil
"Denote for daily journaling."
:group 'denote
:link '(info-link "(denote) Top")
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
(defcustom denote-journal-extras-directory
(expand-file-name "journal" denote-directory)
"Directory for storing daily journal entries.
This can either be the same as the variable `denote-directory' or
a subdirectory of it.
A value of nil means to use the variable `denote-directory'.
Journal entries will thus be in a flat listing together with all
other notes. They can still be retrieved easily by searching for
the `denote-journal-extras-keyword'."
:group 'denote-journal-extras
:type '(choice (directory :tag "Provide directory path (is created if missing)")
(const :tag "Use the `denote-directory'" nil)))
(defcustom denote-journal-extras-keyword "journal"
"Single word keyword to tag journal entries.
It is used by `denote-journal-extras-new-entry' to add a keyword
to the newly created file."
:group 'denote-journal-extras
:type 'string)
(defcustom denote-journal-extras-title-format 'day-date-month-year-24h
"Date format to construct the title with `denote-journal-extras-new-entry'.
The value is either a symbol or an arbitrary string that is
passed to `format-time-string' (consult its documentation for the
technicalities).
Acceptable symbols and their corresponding styles are:
| Symbol | Style |
|-------------------------+-----------------------------------|
| day | Monday |
| day-date-month-year | Monday 19 September 2023 |
| day-date-month-year-24h | Monday 19 September 2023 20:49 |
| day-date-month-year-12h | Monday 19 September 2023 08:49 PM |
With a nil value, make `denote-journal-extras-new-entry' prompt
for a title."
:group 'denote-journal-extras
:type '(choice
(const :tag "Prompt for title with `denote-journal-extras-new-entry'" nil)
(const :tag "Monday"
:doc "The `format-time-string' is: %A"
day)
(const :tag "Monday 19 September 2023"
:doc "The `format-time-string' is: %A %e %B %Y"
day-date-month-year)
(const :tag "Monday 19 September 2023 20:49"
:doc "The `format-time-string' is: %A %e %B %Y %H:%M"
day-date-month-year-24h)
(const :tag "Monday 19 September 2023 08:49 PM"
:doc "The `format-time-string' is: %A %e %B %Y %I:%M %^p"
day-date-month-year-12h)
(string :tag "Custom string with `format-time-string' specifiers")))
(defcustom denote-journal-extras-hook nil
"Normal hook called after `denote-journal-extras-new-entry'.
Use this to, for example, set a timer after starting a new
journal entry (refer to the `tmr' package on GNU ELPA)."
:group 'denote-journal-extras
:type 'hook)
(defun denote-journal-extras-directory ()
"Make the variable `denote-journal-extras-directory' and its parents."
(if-let (((stringp denote-journal-extras-directory))
(directory (file-name-as-directory (expand-file-name denote-journal-extras-directory))))
(progn
(when (not (file-directory-p denote-journal-extras-directory))
(make-directory directory :parents))
directory)
(denote-directory)))
(defun denote-journal-extras-daily--title-format (&optional date)
"Return present date in `denote-journal-extras-title-format' or prompt for title.
With optional DATE, use it instead of the present date. DATE has
the same format as that returned by `current-time'."
(format-time-string
(if (and denote-journal-extras-title-format
(stringp denote-journal-extras-title-format))
denote-journal-extras-title-format
(pcase denote-journal-extras-title-format
('day "%A")
('day-date-month-year "%A %e %B %Y")
('day-date-month-year-24h "%A %e %B %Y %H:%M")
('day-date-month-year-12h "%A %e %B %Y %I:%M %^p")
(_ (denote-title-prompt (format-time-string "%F" date)))))
date))
(defun denote-journal-extras--get-template ()
"Return template that has `journal' key in `denote-templates'.
If no template with `journal' key exists but `denote-templates'
is non-nil, prompt the user for a template among
`denote-templates'. Else return nil.
Also see `denote-journal-extras-new-entry'."
(if-let ((template (alist-get 'journal denote-templates)))
template
(when denote-templates
(denote-template-prompt))))
;;;###autoload
(defun denote-journal-extras-new-entry (&optional date)
"Create a new journal entry in variable `denote-journal-extras-directory'.
Use `denote-journal-extras-keyword' as a keyword for the newly
created file. Set the title of the new entry according to the
value of the user option `denote-journal-extras-title-format'.
With optional DATE as a prefix argument, prompt for a date. If
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
date selection module.
When called from Lisp DATE is a string and has the same format as
that covered in the documentation of the `denote' function. It
is internally processed by `denote-parse-date'."
(interactive (list (when current-prefix-arg (denote-date-prompt))))
(let ((internal-date (denote-parse-date date))
(denote-directory (denote-journal-extras-directory)))
(denote
(denote-journal-extras-daily--title-format internal-date)
`(,denote-journal-extras-keyword)
nil nil date
(denote-journal-extras--get-template))
(run-hooks 'denote-journal-extras-hook)))
(defun denote-journal-extras--entry-today (&optional date)
"Return list of files matching a journal for today or optional DATE.
DATE has the same format as that returned by `denote-parse-date'."
(let* ((identifier (format "%sT[0-9]\\{6\\}" (format-time-string "%Y%m%d" date)))
(files (denote-directory-files identifier))
(keyword (concat "_" (regexp-quote denote-journal-extras-keyword))))
(seq-filter
(lambda (file)
(string-match-p keyword file))
files)))
;;;###autoload
(defun denote-journal-extras-new-or-existing-entry (&optional date)
"Locate an existing journal entry or create a new one.
A journal entry is one that has `denote-journal-extras-keyword' as
part of its file name.
If there are multiple journal entries for the current date,
prompt for one using minibuffer completion. If there is only
one, visit it outright. If there is no journal entry, create one
by calling `denote-journal-extra-new-entry'.
With optional DATE as a prefix argument, prompt for a date. If
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
date selection module.
When called from Lisp, DATE is a string and has the same format
as that covered in the documentation of the `denote' function.
It is internally processed by `denote-parse-date'."
(interactive
(list
(when current-prefix-arg
(denote-date-prompt))))
(let* ((internal-date (denote-parse-date date))
(files (denote-journal-extras--entry-today internal-date)))
(cond
((length> files 1)
(find-file (completing-read "Select journal entry: " files nil :require-match)))
(files
(find-file (car files)))
(t
(denote-journal-extras-new-entry date)))))
;;;###autoload
(defun denote-journal-extras-link-or-create-entry (&optional date id-only)
"Use `denote-link' on journal entry, creating it if necessary.
A journal entry is one that has `denote-journal-extras-keyword' as
part of its file name.
If there are multiple journal entries for the current date,
prompt for one using minibuffer completion. If there is only
one, link to it outright. If there is no journal entry, create one
by calling `denote-journal-extra-new-entry' and link to it.
With optional DATE as a prefix argument, prompt for a date. If
`denote-date-prompt-use-org-read-date' is non-nil, use the Org
date selection module.
When called from Lisp, DATE is a string and has the same format
as that covered in the documentation of the `denote' function.
It is internally processed by `denote-parse-date'.
With optional ID-ONLY as a prefix argument create a link that
consists of just the identifier. Else try to also include the
file's title. This has the same meaning as in `denote-link'."
(interactive
(pcase current-prefix-arg
('(16) (list (denote-date-prompt) :id-only))
('(4) (list (denote-date-prompt)))))
(let* ((internal-date (denote-parse-date date))
(files (denote-journal-extras--entry-today internal-date))
(path))
(cond
((length> files 1)
(setq path (completing-read "Select journal entry: " files nil :require-match)))
(files
(setq path (car files)))
(t
(save-window-excursion
(denote-journal-extras-new-entry date)
(save-buffer)
(setq path (buffer-file-name)))))
(denote-link path
(denote-filetype-heuristics (buffer-file-name))
(denote--link-get-description path)
id-only)))
(provide 'denote-journal-extras)
;;; denote-journal-extras.el ends here

70
denote-org-dblock.el Normal file
View file

@ -0,0 +1,70 @@
;;; denote-org-dblock.el --- Compatibility alieases for Denote Org Dynamic blocks -*- lexical-binding: t -*-
;; Copyright (C) 2022-2024 Free Software Foundation, Inc.
;; Authors: Elias Storms <elias.storms@gmail.com>,
;; Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Protesilaos Stavrou <info@protesilaos.com>
;; Deprecated-since: 3.0.0
;; 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:
;;
;; This file defines compatibility aliases for Org dynamic blocks set
;; up by Denote. The new source of these is the denote-org-extras.el.
;;
;; Below is the old commentary.
;;
;; * * *
;;
;; This file defines Org dynamic blocks using the facility described
;; in the Org manual. Evaluate this:
;;
;; (info "(org) Dynamic Blocks")
;;
;; The dynamic blocks defined herein are documented at length in the
;; Denote manual. See the following node and its subsections:
;;
;; (info "(denote) Use Org dynamic blocks")
;;; Code:
(require 'denote-org-extras)
(define-obsolete-function-alias
'denote-org-dblock-insert-links
'denote-org-extras-dblock-insert-links
"3.0.0")
(define-obsolete-function-alias
'denote-org-dblock-insert-backlinks
'denote-org-extras-dblock-insert-backlinks
"3.0.0")
(define-obsolete-function-alias
'denote-org-dblock-insert-files
'denote-org-extras-dblock-insert-files
"3.0.0")
(display-warning
'denote
"`denote-org-dblock.el' is obsolete; use `denote-org-extras.el'"
:warning)
(provide 'denote-org-dblock)
;;; denote-org-dblock.el ends here

590
denote-org-extras.el Normal file
View file

@ -0,0 +1,590 @@
;;; denote-org-extras.el --- Denote extensions for Org mode -*- lexical-binding: t -*-
;; Copyright (C) 2024 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Protesilaos Stavrou <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:
;;
;; WORK-IN-PROGRESS
;;; Code:
(require 'denote)
(require 'denote-sort)
(require 'org)
;;;; Link to file and heading
(defun denote-org-extras--get-outline (file)
"Return `outline-regexp' headings and line numbers of FILE."
(with-current-buffer (find-file-noselect file)
(let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+")))
candidates)
(save-excursion
(goto-char (point-min))
(while (if (bound-and-true-p outline-search-function)
(funcall outline-search-function)
(re-search-forward outline-regexp nil t))
(push
;; NOTE 2024-01-20: The -5 (minimum width) is a
;; sufficiently high number to keep the alignment
;; consistent in most cases. Larger files will simply
;; shift the heading text in minibuffer, but this is not an
;; issue anymore.
(format "%-5s %s"
(line-number-at-pos (point))
(buffer-substring-no-properties (line-beginning-position) (line-end-position)))
candidates)
(goto-char (1+ (line-end-position)))))
(if candidates
(nreverse candidates)
(user-error "No outline")))))
(define-obsolete-function-alias
'denote-org-extras--outline-prompt
'denote-org-extras-outline-prompt
"3.1.0")
(defun denote-org-extras-outline-prompt (&optional file)
"Prompt for outline among headings retrieved by `denote-org-extras--get-outline'.
With optional FILE use the outline of it, otherwise use that of
the current file."
(let ((current-file (or file buffer-file-name)))
(completing-read
(format "Select heading inside `%s': "
(propertize (file-name-nondirectory current-file) 'face 'denote-faces-prompt-current-name))
(denote--completion-table-no-sort 'imenu (denote-org-extras--get-outline current-file))
nil :require-match)))
(defun denote-org-extras--get-heading-and-id-from-line (line file)
"Return heading text and CUSTOM_ID from the given LINE in FILE."
(with-current-buffer (find-file-noselect file)
(save-excursion
(goto-char (point-min))
(forward-line (1- line))
(cons (denote-link-ol-get-heading) (denote-link-ol-get-id)))))
(defun denote-org-extras-format-link-with-heading (file heading-id description)
"Prepare link to FILE with HEADING-ID using DESCRIPTION."
(when (region-active-p)
(setq description (buffer-substring-no-properties (region-beginning) (region-end)))
(denote--delete-active-region-content))
(format "[[denote:%s::#%s][%s]]"
(denote-retrieve-filename-identifier file)
heading-id
description))
;;;###autoload
(defun denote-org-extras-link-to-heading ()
"Link to file and then specify a heading to extend the link to.
The resulting link has the following pattern:
[[denote:IDENTIFIER::#ORG-HEADING-CUSTOM-ID]][Description::Heading text]].
Because only Org files can have links to individual headings,
limit the list of possible files to those which include the .org
file extension (remember that Denote works with many file types,
per the user option `denote-file-type').
The user option `denote-org-extras-store-link-to-heading'
determined whether the `org-store-link' function can save a link
to the current heading. Such links look the same as those of
this command, though the functionality defined herein is
independent of it.
To only link to a file, use the `denote-link' command.
Also see `denote-org-extras-backlinks-for-heading'."
(declare (interactive-only t))
(interactive nil org-mode)
(unless (derived-mode-p 'org-mode)
(user-error "Links to headings only work between Org files"))
(when-let ((file (denote-file-prompt ".*\\.org"))
(file-text (denote--link-get-description file))
(heading (denote-org-extras-outline-prompt file))
(line (string-to-number (car (split-string heading "\t"))))
(heading-data (denote-org-extras--get-heading-and-id-from-line line file))
(heading-text (car heading-data))
(heading-id (cdr heading-data))
(description (denote-link-format-heading-description file-text heading-text)))
(insert (denote-org-extras-format-link-with-heading file heading-id description))))
;;;; Heading backlinks
(defun denote-org-extras--get-file-id-and-heading-id (file)
"Return IDENTIFIER::#ORG-HEADING-CUSTOM-ID string for FILE heading at point."
(if-let ((heading-id (org-entry-get (point) "CUSTOM_ID")))
(concat (denote-retrieve-filename-identifier-with-error file) "::#" heading-id)
(error "No CUSTOM_ID for heading at point in file `%s'" file)))
(defun denote-org-extras--get-backlinks-buffer-name (text)
"Format a buffer name for `denote-org-extras-backlinks-for-heading' with TEXT."
(format "*Denote HEADING backlinks for %S*" text))
;;;###autoload
(defun denote-org-extras-backlinks-for-heading ()
"Produce backlinks for the current heading.
This otherwise has the same behaviour as `denote-backlinks'---refer to
that for the details.
Also see `denote-org-extras-link-to-heading'."
(interactive)
(when-let ((heading-id (denote-org-extras--get-file-id-and-heading-id buffer-file-name))
(heading-text (substring-no-properties (denote-link-ol-get-heading))))
(denote-link--prepare-backlinks heading-id ".*\\.org" (denote-org-extras--get-backlinks-buffer-name heading-text))))
;;;; Extract subtree into its own note
(defun denote-org-extras--get-heading-date ()
"Try to return a timestamp for the current Org heading.
This can be used as the value for the DATE argument of the
`denote' command."
(when-let ((pos (point))
(timestamp (or (org-entry-get pos "DATE")
(org-entry-get pos "CREATED")
(org-entry-get pos "CLOSED"))))
(date-to-time timestamp)))
;;;###autoload
(defun denote-org-extras-extract-org-subtree ()
"Create new Denote note using the current Org subtree as input.
Remove the subtree from its current file and move its contents
into a new Denote file (a subtree is a heading with all of its
contents, including subheadings).
Take the text of the subtree's top level heading and use it as
the title of the new note.
If the heading has any tags, use them as the keywords of the new
note. If the Org file has any #+filetags use them as well (Org's
filetags are inherited by the headings). If none of these are
true and the user option `denote-prompts' includes an entry for
keywords, then prompt for keywords. Else do not include any
keywords.
If the heading has a PROPERTIES drawer, retain it for further
review.
If the heading's PROPERTIES drawer includes a DATE or CREATED
property, or there exists a CLOSED statement with a timestamp
value, use that to derive the date (or date and time) of the new
note (if there is only a date, the time is taken as 00:00). If
more than one of these is present, the order of preference is
DATE, then CREATED, then CLOSED. If none of these is present,
use the current time. If the `denote-prompts' includes an entry
for a date, then prompt for a date at this stage (also see
`denote-date-prompt-use-org-read-date').
For the rest, consult the value of the user option
`denote-prompts' in the following scenaria:
- Optionally prompt for a subdirectory, otherwise produce the new
note in the variable `denote-directory'.
- Optionally prompt for a file signature, otherwise do not use
one.
Make the new note an Org file regardless of the value of
`denote-file-type'."
(interactive nil org-mode)
(unless (derived-mode-p 'org-mode)
(user-error "Headings can only be extracted from Org files"))
(if-let ((text (org-get-entry))
(heading (denote-link-ol-get-heading)))
(let ((tags (org-get-tags))
(date (denote-org-extras--get-heading-date))
subdirectory
signature)
(dolist (prompt denote-prompts)
(pcase prompt
('keywords (when (not tags)
(setq tags (denote-keywords-prompt))))
('subdirectory (setq subdirectory (denote-subdirectory-prompt)))
('date (when (not date) (setq date (denote-date-prompt))))
('signature (setq signature (denote-signature-prompt)))))
(delete-region (org-entry-beginning-position)
(save-excursion (org-end-of-subtree t) (point)))
(denote heading tags 'org subdirectory date nil signature)
(insert text))
(user-error "No subtree to extract; aborting")))
;;;; Convert links from `:denote' to `:file' and vice versa
;; TODO 2024-02-28: Do we need to convert between other link types? I
;; think not, since the `denote:' type is modelled after the `file:'
;; one.
(defun denote-org-extras--get-link-type-regexp (type)
"Return regexp for Org link TYPE.
TYPE is a symbol of either `file' or `denote'.
The regexp consists of four groups. Group 1 is the link type, 2
is the target, 3 is the target's search terms, and 4 is the
description."
(let ((group-1))
(pcase type
('denote (setq group-1 "denote"))
('file (setq group-1 "file"))
(_ (error "`%s' is an unknown link type" type)))
(format "\\[\\[\\(?1:%s:\\)\\(?:\\(?2:.*?\\)\\(?3:::.*\\)?\\]\\|\\]\\)\\(?4:\\[\\(?:.*?\\)\\]\\)?\\]" group-1)))
(defun denote-org-extras--get-path (id)
"Return file path to ID according to `org-link-file-path-type'."
(if (or (eq org-link-file-path-type 'adaptive)
(eq org-link-file-path-type 'relative))
(denote-get-relative-path-by-id id)
(denote-get-path-by-id id)))
;;;###autoload
(defun denote-org-extras-convert-links-to-file-type ()
"Convert denote: links to file: links in the current Org buffer.
Ignore all other link types. Also ignore links that do not
resolve to a file in the variable `denote-directory'."
(interactive nil org-mode)
(if (derived-mode-p 'org-mode)
(progn
(goto-char (point-min))
(while (re-search-forward (denote-org-extras--get-link-type-regexp 'denote) nil :no-error)
(let* ((id (match-string-no-properties 2))
(search (or (match-string-no-properties 3) ""))
(desc (or (match-string-no-properties 4) ""))
(file (save-match-data (denote-org-extras--get-path id))))
(when id
(let ((new-text (if desc
(format "[[file:%s%s]%s]" file search desc)
(format "[[file:%s%s]]" file search))))
(replace-match new-text :fixed-case :literal)))))
;; TODO 2024-02-28: notify how many changed.
(message "Converted `denote:' links to `file:' links"))
(user-error "The current file is not using Org mode")))
;;;###autoload
(defun denote-org-extras-convert-links-to-denote-type ()
"Convert file: links to denote: links in the current Org buffer.
Ignore all other link types. Also ignore file: links that do not
point to a file with a Denote file name."
(interactive nil org-mode)
(if (derived-mode-p 'org-mode)
(progn
(goto-char (point-min))
(while (re-search-forward (denote-org-extras--get-link-type-regexp 'file) nil :no-error)
(let* ((file (match-string-no-properties 2))
(search (or (match-string-no-properties 3) ""))
(desc (or (match-string-no-properties 4) ""))
(id (save-match-data (denote-retrieve-filename-identifier file))))
(when id
(let ((new-text (if desc
(format "[[denote:%s%s]%s]" id search desc)
(format "[[denote:%s%s]]" id search))))
(replace-match new-text :fixed-case :literal)))))
;; TODO 2024-02-28: notify how many changed.
(message "Converted as `file:' links to `denote:' links"))
(user-error "The current file is not using Org mode")))
;;;; Org dynamic blocks
;; NOTE 2024-01-22 12:26:13 +0200: The following is copied from the
;; now-deleted denote-org-dblock.el. Its original author was Elias
;; Storms <elias.storms@gmail.com>, with substantial contributions and
;; further developments by me (Protesilaos).
;; This section defines Org dynamic blocks using the facility described
;; in the Org manual. Evaluate this:
;;
;; (info "(org) Dynamic Blocks")
;;
;; The dynamic blocks defined herein are documented at length in the
;; Denote manual. See the following node and its subsections:
;;
;; (info "(denote) Use Org dynamic blocks")
;;;;; Common helper functions
(defun denote-org-extras-dblock--files (files-matching-regexp &optional sort-by-component reverse)
"Return list of FILES-MATCHING-REGEXP in variable `denote-directory'.
SORT-BY-COMPONENT and REVERSE have the same meaning as
`denote-sort-files'. If both are nil, do not try to perform any
sorting.
Also see `denote-org-extras-dblock--files-missing-only'."
(cond
((and sort-by-component reverse)
(denote-sort-get-directory-files files-matching-regexp sort-by-component reverse :omit-current))
(sort-by-component
(denote-sort-get-directory-files files-matching-regexp sort-by-component nil :omit-current))
(reverse
(denote-sort-get-directory-files files-matching-regexp :no-component-specified reverse :omit-current))
(t
(denote-directory-files files-matching-regexp :omit-current))))
(defun denote-org-extras-dblock--get-missing-links (regexp)
"Return list of missing links to all notes matching REGEXP.
Missing links are those for which REGEXP does not have a match in
the current buffer."
(let ((found-files (denote-directory-files regexp :omit-current))
(linked-files (denote-link--expand-identifiers denote-org-link-in-context-regexp)))
(if-let ((final-files (seq-difference found-files linked-files)))
final-files
(message "All links matching `%s' are present" regexp)
'())))
(defun denote-org-extras-dblock--files-missing-only (files-matching-regexp &optional sort-by-component reverse)
"Return list of missing links to FILES-MATCHING-REGEXP.
SORT-BY-COMPONENT and REVERSE have the same meaning as
`denote-sort-files'. If both are nil, do not try to perform any
sorting.
Also see `denote-org-extras-dblock--files'."
(denote-sort-files
(denote-org-extras-dblock--get-missing-links files-matching-regexp)
sort-by-component
reverse))
;;;;; Dynamic block to insert links
;;;###autoload
(defun denote-org-extras-dblock-insert-links (regexp)
"Create Org dynamic block to insert Denote links matching REGEXP."
(interactive
(list
(denote-files-matching-regexp-prompt))
org-mode)
(org-create-dblock (list :name "denote-links"
:regexp regexp
:sort-by-component nil
:reverse-sort nil
:id-only nil))
(org-update-dblock))
;; NOTE 2024-03-30: This is how the autoload is done in org.el.
;;;###autoload
(eval-after-load 'org
'(progn
(org-dynamic-block-define "denote-links" 'denote-org-extras-dblock-insert-links)))
;;;###autoload
(defun org-dblock-write:denote-links (params)
"Function to update `denote-links' Org Dynamic blocks.
Used by `org-dblock-update' with PARAMS provided by the dynamic block."
(let* ((regexp (plist-get params :regexp))
(rx (if (listp regexp) (macroexpand `(rx ,regexp)) regexp))
(sort (plist-get params :sort-by-component))
(reverse (plist-get params :reverse-sort))
(block-name (plist-get params :block-name))
(files (denote-org-extras-dblock--files rx sort reverse)))
(when block-name (insert "#+name: " block-name "\n"))
(denote-link--insert-links files 'org (plist-get params :id-only) :no-other-sorting)
(join-line))) ; remove trailing empty line
;;;;; Dynamic block to insert missing links
;;;###autoload
(defun denote-org-extras-dblock-insert-missing-links (regexp)
"Create Org dynamic block to insert Denote links matching REGEXP."
(interactive
(list
(denote-files-matching-regexp-prompt))
org-mode)
(org-create-dblock (list :name "denote-missing-links"
:regexp regexp
:sort-by-component nil
:reverse-sort nil
:id-only nil))
(org-update-dblock))
;; NOTE 2024-03-30: This is how the autoload is done in org.el.
;;;###autoload
(eval-after-load 'org
'(progn
(org-dynamic-block-define "denote-missing-links" 'denote-org-extras-dblock-insert-links)))
;;;###autoload
(defun org-dblock-write:denote-missing-links (params)
"Function to update `denote-links' Org Dynamic blocks.
Used by `org-dblock-update' with PARAMS provided by the dynamic block."
(let* ((regexp (plist-get params :regexp))
(rx (if (listp regexp) (macroexpand `(rx ,regexp)) regexp))
(sort (plist-get params :sort-by-component))
(reverse (plist-get params :reverse-sort))
(block-name (plist-get params :block-name))
(files (denote-org-extras-dblock--files-missing-only rx sort reverse)))
(when block-name (insert "#+name: " block-name "\n"))
(denote-link--insert-links files 'org (plist-get params :id-only) :no-other-sorting)
(join-line))) ; remove trailing empty line
;;;;; Dynamic block to insert backlinks
(defun denote-org-extras-dblock--maybe-sort-backlinks (files sort-by-component reverse)
"Sort backlink FILES if SORT-BY-COMPONENT and/or REVERSE is non-nil."
(cond
((and sort-by-component reverse)
(denote-sort-files files sort-by-component reverse))
(sort-by-component
(denote-sort-files files sort-by-component))
(reverse
(denote-sort-files files :no-component-specified reverse))
(t
files)))
;;;###autoload
(defun denote-org-extras-dblock-insert-backlinks ()
"Create Org dynamic block to insert Denote backlinks to current file."
(interactive nil org-mode)
(org-create-dblock (list :name "denote-backlinks"
:sort-by-component nil
:reverse-sort nil
:id-only nil))
(org-update-dblock))
;; NOTE 2024-03-30: This is how the autoload is done in org.el.
;;;###autoload
(eval-after-load 'org
'(progn
(org-dynamic-block-define "denote-backlinks" 'denote-org-extras-dblock-insert-backlinks)))
;;;###autoload
(defun org-dblock-write:denote-backlinks (params)
"Function to update `denote-backlinks' Org Dynamic blocks.
Used by `org-dblock-update' with PARAMS provided by the dynamic block."
(when-let ((files (denote-link-return-backlinks)))
(let* ((sort (plist-get params :sort-by-component))
(reverse (plist-get params :reverse-sort))
(files (denote-org-extras-dblock--maybe-sort-backlinks files sort reverse)))
(denote-link--insert-links files 'org (plist-get params :id-only) :no-other-sorting)
(join-line)))) ; remove trailing empty line
;;;;; Dynamic block to insert entire file contents
(defun denote-org-extras-dblock--get-file-contents (file &optional no-front-matter add-links)
"Insert the contents of FILE.
With optional NO-FRONT-MATTER as non-nil, try to remove the front
matter from the top of the file. If NO-FRONT-MATTER is a number,
remove that many lines starting from the top. If it is any other
non-nil value, delete from the top until the first blank line.
With optional ADD-LINKS as non-nil, first insert a link to the
file and then insert its contents. In this case, format the
contents as a typographic list. If ADD-LINKS is `id-only', then
insert links as `denote-link' does when supplied with an ID-ONLY
argument."
(when (denote-file-is-note-p file)
(with-temp-buffer
(when add-links
(insert
(format "- %s\n\n"
(denote-format-link
file
(denote--link-get-description file)
'org
(eq add-links 'id-only)))))
(let ((beginning-of-contents (point)))
(insert-file-contents file)
(when no-front-matter
(delete-region
(if (natnump no-front-matter)
(progn (forward-line no-front-matter) (line-beginning-position))
(1+ (re-search-forward "^$" nil :no-error 1)))
beginning-of-contents))
(when add-links
(indent-region beginning-of-contents (point-max) 2)))
(buffer-string))))
(defvar denote-org-extras-dblock-file-contents-separator
(concat "\n\n" (make-string 50 ?-) "\n\n\n")
"Fallback separator used by `denote-org-extras-dblock-add-files'.")
(defun denote-org-extras-dblock--separator (separator)
"Return appropriate value of SEPARATOR for `denote-org-extras-dblock-add-files'."
(cond
((null separator) "")
((stringp separator) separator)
(t denote-org-extras-dblock-file-contents-separator)))
(defun denote-org-extras-dblock-add-files (regexp &optional separator no-front-matter add-links sort-by-component reverse)
"Insert files matching REGEXP.
Seaprate them with the optional SEPARATOR. If SEPARATOR is nil,
use the `denote-org-extras-dblock-file-contents-separator'.
If optional NO-FRONT-MATTER is non-nil try to remove the front
matter from the top of the file. Do it by finding the first
blank line, starting from the top of the buffer.
If optional ADD-LINKS is non-nil, first insert a link to the file
and then insert its contents. In this case, format the contents
as a typographic list.
If optional SORT-BY-COMPONENT is a symbol among `denote-sort-components',
sort files matching REGEXP by the corresponding Denote file name
component. If the symbol is not among `denote-sort-components',
fall back to the default identifier-based sorting.
If optional REVERSE is non-nil reverse the sort order."
(let* ((files (denote-org-extras-dblock--files regexp sort-by-component reverse))
(files-contents (mapcar
(lambda (file) (denote-org-extras-dblock--get-file-contents file no-front-matter add-links))
files)))
(insert (string-join files-contents (denote-org-extras-dblock--separator separator)))))
;;;###autoload
(defun denote-org-extras-dblock-insert-files (regexp sort-by-component)
"Create Org dynamic block to insert Denote files matching REGEXP.
Sort the files according to SORT-BY-COMPONENT, which is a symbol
among `denote-sort-components'."
(interactive
(list
(denote-files-matching-regexp-prompt)
(denote-sort-component-prompt))
org-mode)
(org-create-dblock (list :name "denote-files"
:regexp regexp
:sort-by-component sort-by-component
:reverse-sort nil
:no-front-matter nil
:file-separator nil
:add-links nil))
(org-update-dblock))
;; NOTE 2024-03-30: This is how the autoload is done in org.el.
;;;###autoload
(eval-after-load 'org
'(progn
(org-dynamic-block-define "denote-files" 'denote-org-extras-dblock-insert-files)))
;;;###autoload
(defun org-dblock-write:denote-files (params)
"Function to update `denote-files' Org Dynamic blocks.
Used by `org-dblock-update' with PARAMS provided by the dynamic block."
(let* ((regexp (plist-get params :regexp))
(rx (if (listp regexp) (macroexpand `(rx ,regexp)) regexp))
(sort (plist-get params :sort-by-component))
(reverse (plist-get params :reverse-sort))
(block-name (plist-get params :block-name))
(separator (plist-get params :file-separator))
(no-f-m (plist-get params :no-front-matter))
(add-links (plist-get params :add-links)))
(when block-name (insert "#+name: " block-name "\n"))
(when rx (denote-org-extras-dblock-add-files rx separator no-f-m add-links sort reverse)))
(join-line)) ; remove trailing empty line
(provide 'denote-org-extras)
;;; denote-org-extras.el ends here

159
denote-rename-buffer.el Normal file
View file

@ -0,0 +1,159 @@
;;; denote-rename-buffer.el --- Rename Denote buffers to be shorter and easier to read -*- lexical-binding: t -*-
;; Copyright (C) 2023-2024 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Protesilaos Stavrou <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:
;;
;; Rename Denote buffers to be shorter and easier to read. Enable
;; `denote-rename-buffer-mode' to automatically rename the buffer of a
;; Denote file. The renaming function is specified in the user option
;; `denote-rename-buffer-function'.
;;; Code:
(require 'denote)
(defgroup denote-rename-buffer nil
"Rename Denote buffers to be shorter and easier to read."
:group 'denote
:link '(info-link "(denote) Top")
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
(defcustom denote-rename-buffer-format "%t"
"The format of the buffer name `denote-rename-buffer' should use.
Thie value is a string that treats specially the following
specifiers:
- The %t is the Denote TITLE of the file.
- The %i is the Denote IDENTIFIER of the file.
- The %d is the same as %i (DATE mnemonic).
- The %s is the Denote SIGNATURE of the file.
- The %k is the Denote KEYWORDS of the file.
- The %% is a literal percent sign.
In addition, the following flags are available for each of the specifiers:
- 0 :: Pad to the width, if given, with zeros instead of spaces.
- - :: Pad to the width, if given, on the right instead of the left.
- < :: Truncate to the width and precision, if given, on the left.
- > :: Truncate to the width and precision, if given, on the right.
- ^ :: Convert to upper case.
- _ :: Convert to lower case.
When combined all together, the above are written thus:
%<flags><width><precision>SPECIFIER-CHARACTER
Any other string it taken as-is. Users may want, for example, to
include some text that makes Denote buffers stand out, such as
a [D] prefix."
:type 'string
:package-version '(denote . "2.1.0")
:group 'denote-rename-buffer)
(defcustom denote-rename-buffer-function #'denote-rename-buffer
"Symbol of function that is called to rename the Denote file buffer.
The default `denote-rename-buffer' function uses the pattern
described in `denote-rename-buffer-format'.
Users can set this variable to an arbitrary function that does
something else. The function is called without arguments from
the `find-file-hook' and `denote-after-new-note-hook'.
A nil value for this variable means that the title of the Denote
buffer will be used, if available."
:type '(choice
(const :tag "Rename using the `denote-rename-buffer-format'" denote-rename-buffer)
(function :tag "Use a custom renaming function"))
:package-version '(denote . "2.1.0")
:group 'denote-rename-buffer)
(defun denote-rename-buffer--format (buffer)
"Parse the BUFFER through the `denote-rename-buffer-format'."
(when-let ((file (buffer-file-name buffer))
(type (denote-filetype-heuristics file)))
(string-trim
(format-spec denote-rename-buffer-format
(list (cons ?t (cond
((denote-retrieve-front-matter-title-value file type))
((denote-retrieve-filename-title file))
(t "")))
(cons ?i (or (denote-retrieve-filename-identifier file) ""))
(cons ?d (or (denote-retrieve-filename-identifier file) ""))
(cons ?s (or (denote-retrieve-filename-signature file) ""))
(cons ?k (if-let ((kws (denote-retrieve-front-matter-keywords-value file type)))
(denote-keywords-combine kws)
(or (denote-retrieve-filename-keywords file) "")))
(cons ?% "%"))
'delete))))
(defun denote-rename-buffer (&optional buffer)
"Rename current buffer or optional BUFFER with `denote-rename-buffer-format'.
The symbol of this function is the default value of the user
option `denote-rename-buffer-function' and is thus used by the
`denote-rename-buffer-mode'."
(when-let ((file (buffer-file-name buffer))
((denote-file-has-identifier-p file))
(new-name (denote-rename-buffer--format (or buffer (current-buffer))))
((not (string-blank-p new-name))))
(rename-buffer new-name :unique)))
(make-obsolete
'denote-rename-buffer-with-title
'denote-rename-buffer
"2.1.0")
(make-obsolete
'denote-rename-buffer-with-identifier
'denote-rename-buffer
"2.1.0")
(defun denote-rename-buffer--fallback (&optional buffer)
"Fallback to rename BUFFER or `current-buffer'.
This is called if `denote-rename-buffer-rename-function' is nil."
(let ((denote-rename-buffer-format "%t"))
(denote-rename-buffer buffer)))
(defun denote-rename-buffer-rename-function-or-fallback ()
"Call `denote-rename-buffer-function' or its fallback to rename with title.
Add this to `find-file-hook' and `denote-after-new-note-hook'."
(funcall (or denote-rename-buffer-function #'denote-rename-buffer--fallback)))
;;;###autoload
(define-minor-mode denote-rename-buffer-mode
"Automatically rename Denote buffers to be easier to read.
A buffer is renamed upon visiting the underlying file. This
means that existing buffers are not renamed until they are
visited again in a new buffer (files are visited with the command
`find-file' or related)."
:global t
(if denote-rename-buffer-mode
(progn
(add-hook 'denote-after-new-note-hook #'denote-rename-buffer-rename-function-or-fallback)
(add-hook 'denote-after-rename-file-hook #'denote-rename-buffer-rename-function-or-fallback)
(add-hook 'find-file-hook #'denote-rename-buffer-rename-function-or-fallback))
(remove-hook 'denote-after-new-note-hook #'denote-rename-buffer-rename-function-or-fallback)
(remove-hook 'denote-after-rename-file-hook #'denote-rename-buffer-rename-function-or-fallback)
(remove-hook 'find-file-hook #'denote-rename-buffer-rename-function-or-fallback)))
(provide 'denote-rename-buffer)
;;; denote-rename-buffer.el ends here

107
denote-silo-extras.el Normal file
View file

@ -0,0 +1,107 @@
;;; denote-silo-extras.el --- Convenience functions for using Denote in multiple silos -*- lexical-binding: t; -*-
;; Copyright (C) 2023-2024 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Protesilaos Stavrou <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:
;; This is a set of convenience functions that used to be provided in
;; the Denote manual. A "silo" is a `denote-directory' that is
;; self-contained. Users can maintain multiple silos. Consult the
;; manual for the details. With the Denote package installed,
;; evaluate the following to read the relevant node:
;;
;; (info "(denote) Maintain separate directory silos for notes")
;;; Code:
(require 'denote)
(defgroup denote-silo-extras nil
"Make it easier to use Denote across Silos."
:group 'denote
:link '(info-link "(denote) Top")
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
(defcustom denote-silo-extras-directories
`(,denote-directory)
"List of file paths pointing to Denote silos.
Each file path points to a directory, which takes the same value
as the variable `denote-directory'."
:group 'denote-silo-extras
:link '(info-link "(denote) Maintain separate directories for notes")
:type '(repeat directory))
(defvar denote-silo-extras-directory-history nil
"Minibuffer history for `denote-silo-extras-directory-prompt'.")
(defalias 'denote-silo-extras--directory-history 'denote-silo-extras-directory-history
"Compatibility alias for `denote-silo-extras-directory-history'.")
(define-obsolete-function-alias
'denote-silo-extras--directory-prompt
'denote-silo-extras-directory-prompt
"3.1.0")
(defun denote-silo-extras-directory-prompt ()
"Prompt for directory among `denote-silo-extras-directories'."
(let ((default (car denote-silo-extras-directory-history)))
(completing-read
(format-prompt "Select a silo" default)
(denote--completion-table 'file denote-silo-extras-directories)
nil :require-match nil 'denote-silo-extras-directory-history)))
;;;###autoload
(defun denote-silo-extras-create-note (silo)
"Select SILO and run `denote' in it.
SILO is a file path from `denote-silo-extras-directories'.
When called from Lisp, SILO is a file system path to a directory."
(interactive (list (denote-silo-extras-directory-prompt)))
(let ((denote-directory silo))
(call-interactively #'denote)))
;;;###autoload
(defun denote-silo-extras-open-or-create (silo)
"Select SILO and run `denote-open-or-create' in it.
SILO is a file path from `denote-silo-extras-directories'.
When called from Lisp, SILO is a file system path to a directory."
(interactive (list (denote-silo-extras-directory-prompt)))
(let ((denote-directory silo))
(call-interactively #'denote-open-or-create)))
;;;###autoload
(defun denote-silo-extras-select-silo-then-command (silo command)
"Select SILO and run Denote COMMAND in it.
SILO is a file path from `denote-silo-extras-directories', while
COMMAND is one among `denote-silo-extras-commands'.
When called from Lisp, SILO is a file system path to a directory."
(interactive
(list
(denote-silo-extras-directory-prompt)
(denote-command-prompt)))
(let ((denote-directory silo))
(call-interactively command)))
(provide 'denote-silo-extras)
;;; denote-silo-extras.el ends here

201
denote-sort.el Normal file
View file

@ -0,0 +1,201 @@
;;; denote-sort.el --- Sort Denote files based on a file name component -*- lexical-binding: t -*-
;; Copyright (C) 2023-2024 Free Software Foundation, Inc.
;; Author: Protesilaos Stavrou <info@protesilaos.com>
;; Maintainer: Protesilaos Stavrou <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:
;;
;; Sort Denote files based on their file name components, namely, the
;; signature, title, or keywords.
;;; Code:
(require 'denote)
(defgroup denote-sort nil
"Sort Denote files based on a file name component."
:group 'denote
:link '(info-link "(denote) Top")
:link '(url-link :tag "Homepage" "https://protesilaos.com/emacs/denote"))
(defvar denote-sort-comparison-function #'string-collate-lessp
"String comparison function used by `denote-sort-files' subroutines.")
(defvar denote-sort-components '(title keywords signature identifier)
"List of sorting keys applicable for `denote-sort-files' and related.")
;; NOTE 2023-12-04: We can have compound sorting algorithms such as
;; title+signature, but I want to keep this simple for the time being.
;; Let us first hear from users to understand if there is a real need
;; for such a feature.
(defmacro denote-sort--define-lessp (component)
"Define function to sort by COMPONENT."
(let ((retrieve-fn (intern (format "denote-retrieve-filename-%s" component))))
`(defun ,(intern (format "denote-sort-%s-lessp" component)) (file1 file2)
,(format
"Return smallest between FILE1 and FILE2 based on their %s.
The comparison is done with `denote-sort-comparison-function' between the
two title values."
component)
(let* ((one (,retrieve-fn file1))
(two (,retrieve-fn file2))
(one-empty-p (or (null one) (string-empty-p one)))
(two-empty-p (or (null two) (string-empty-p two))))
(cond
(one-empty-p nil)
((and (not one-empty-p) two-empty-p) one)
(t (funcall denote-sort-comparison-function one two)))))))
;; TODO 2023-12-04: Subject to the above NOTE, we can also sort by
;; directory and by file length.
(denote-sort--define-lessp title)
(denote-sort--define-lessp keywords)
(denote-sort--define-lessp signature)
;;;###autoload
(defun denote-sort-files (files component &optional reverse)
"Returned sorted list of Denote FILES.
With COMPONENT as a symbol among `denote-sort-components',
sort files based on the corresponding file name component.
With COMPONENT as a nil value keep the original date-based
sorting which relies on the identifier of each file name.
With optional REVERSE as a non-nil value, reverse the sort order."
(let* ((files-to-sort (copy-sequence files))
(sort-fn (when component
(pcase component
('title #'denote-sort-title-lessp)
('keywords #'denote-sort-keywords-lessp)
('signature #'denote-sort-signature-lessp))))
(sorted-files (if sort-fn (sort files sort-fn) files-to-sort)))
(if reverse
(reverse sorted-files)
sorted-files)))
(defun denote-sort-get-directory-files (files-matching-regexp sort-by-component &optional reverse omit-current)
"Return sorted list of files in variable `denote-directory'.
With FILES-MATCHING-REGEXP as a string limit files to those
matching the given regular expression.
With SORT-BY-COMPONENT as a symbol among `denote-sort-components',
pass it to `denote-sort-files' to sort by the corresponding file
name component.
With optional REVERSE as a non-nil value, reverse the sort order.
With optional OMIT-CURRENT, do not include the current file in
the list."
(denote-sort-files
(denote-directory-files files-matching-regexp omit-current)
sort-by-component
reverse))
(defun denote-sort-get-links (files-matching-regexp sort-by-component current-file-type id-only &optional reverse)
"Return sorted typographic list of links for FILES-MATCHING-REGEXP.
With FILES-MATCHING-REGEXP as a string, match files stored in the
variable `denote-directory'.
With SORT-BY-COMPONENT as a symbol among `denote-sort-components',
sort FILES-MATCHING-REGEXP by the given Denote file name
component. If SORT-BY-COMPONENT is nil or an unknown non-nil
value, default to the identifier-based sorting.
With CURRENT-FILE-TYPE as a symbol among those specified in
`denote-file-type' (or the `car' of each element in
`denote-file-types'), format the link accordingly. With a nil or
unknown non-nil value, default to the Org notation.
With ID-ONLY as a non-nil value, produce links that consist only
of the identifier, thus deviating from CURRENT-FILE-TYPE.
With optional REVERSE as a non-nil value, reverse the sort order."
(denote-link--prepare-links
(denote-sort-get-directory-files files-matching-regexp sort-by-component reverse)
current-file-type
id-only))
(defvar denote-sort-component-history nil
"Minibuffer history of `denote-sort-component-prompt'.")
(defalias 'denote-sort--component-hist 'denote-sort-component-history
"Compatibility alias for `denote-sort-component-history'.")
(defun denote-sort-component-prompt ()
"Prompt `denote-sort-files' for sorting key among `denote-sort-components'."
(let ((default (car denote-sort-component-history)))
(intern
(completing-read
(format-prompt "Sort by file name component" default)
denote-sort-components nil :require-match
nil 'denote-sort-component-history default))))
(defvar-local denote-sort--dired-buffer nil
"Buffer object of current `denote-sort-dired'.")
;;;###autoload
(defun denote-sort-dired (files-matching-regexp sort-by-component reverse)
"Produce Dired dired-buffer with sorted files from variable `denote-directory'.
When called interactively, prompt for FILES-MATCHING-REGEXP,
SORT-BY-COMPONENT, and REVERSE.
1. FILES-MATCHING-REGEXP limits the list of Denote files to
those matching the provided regular expression.
2. SORT-BY-COMPONENT sorts the files by their file name
component (one among `denote-sort-components').
3. REVERSE is a boolean to reverse the order when it is a non-nil value.
When called from Lisp, the arguments are a string, a keyword, and
a non-nil value, respectively."
(interactive
(list
(denote-files-matching-regexp-prompt)
(denote-sort-component-prompt)
(y-or-n-p "Reverse sort? ")))
(if-let ((default-directory (denote-directory))
(files (denote-sort-get-directory-files files-matching-regexp sort-by-component reverse))
;; NOTE 2023-12-04: Passing the FILES-MATCHING-REGEXP as
;; buffer-name produces an error if the regexp contains a
;; wildcard for a directory. I can reproduce this in emacs
;; -Q and am not sure if it is a bug. Anyway, I will report
;; it upstream, but even if it is fixed we cannot use it
;; for now (whatever fix will be available for Emacs 30+).
(denote-sort-dired-buffer-name (format "Denote sort `%s' by `%s'" files-matching-regexp sort-by-component))
(buffer-name (format "Denote sort by `%s' at %s" sort-by-component (format-time-string "%T"))))
(let ((dired-buffer (dired (cons buffer-name (mapcar #'file-relative-name files)))))
(setq denote-sort--dired-buffer dired-buffer)
(with-current-buffer dired-buffer
(setq-local revert-buffer-function
(lambda (&rest _)
(kill-buffer dired-buffer)
(denote-sort-dired files-matching-regexp sort-by-component reverse))))
;; Because of the above NOTE, I am printing a message. Not
;; what I want, but it is better than nothing...
(message denote-sort-dired-buffer-name))
(message "No matching files for: %s" files-matching-regexp)))
(provide 'denote-sort)
;;; denote-sort.el ends here

5867
denote.el

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,11 @@
;;; denote-test.el --- Unit tests for Denote -*- lexical-binding: t -*-
;; Copyright (C) 2023-2026 Free Software Foundation, Inc.
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; Author: Protesilaos <info@protesilaos.com>
;; Maintainer: Protesilaos <info@protesilaos.com>
;; URL: https://github.com/protesilaos/denote
;; 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
;; This file is NOT part of GNU Emacs.
@ -23,182 +24,136 @@
;;; 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")
;; WORK-IN-PROGRESS
;;; 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 ()
(ert-deftest denote-test--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))))
(should (null (denote--make-denote-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'.
(ert-deftest denote-test--denote-directory ()
"Test that variable `denote-directory' returns an absolute directory name."
(let ((path (denote-directory)))
(should (and (file-directory-p path)
(file-name-absolute-p path)))))
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 denote-test--denote--slug-no-punct ()
"Test that `denote--slug-no-punct' removes punctuation from the string.
Concretely, replace with spaces anything that matches the
`denote-excluded-punctuation-regexp' and
`denote-excluded-punctuation-extra-regexp'."
(should (equal (denote--slug-no-punct "This is !@# test")
"This is test")))
(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.
(ert-deftest denote-test--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")))
(should (equal (denote--slug-hyphenate "__ This is a test __ ")
"This-is-a-test")))
(ert-deftest dt-denote-sluggify ()
(ert-deftest denote-test--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")))
(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 =.
(ert-deftest denote-test--denote--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'.
`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")))
(should (equal (denote--slug-put-equals "__ This is a test __ ")
"This=is=a=test")))
(ert-deftest dt-denote-sluggify-signature ()
(ert-deftest denote-test--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")))
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 ()
(ert-deftest denote-test--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")))
(should (equal (denote-sluggify-keyword "--- ___ !~!!$%^ This iS a - tEsT ++ ?? ")
"thisisatest")))
(ert-deftest dt-denote-sluggify-keywords ()
(ert-deftest denote-test--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"))))
(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 ()
(ert-deftest denote-test--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)))
;; (should (null (denote--file-empty-p user-init-file))
(should (let ((file (make-temp-file "denote-test")))
(prog1
(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 denote-test--denote-file-is-note-p ()
"Test that `denote-file-is-note-p' checks that files is a Denote note.
For our purposes, a note must note be a directory, must satisfy
`file-regular-p', its path must be part of the variable
`denote-directory', it must have a Denote identifier in its name,
and use one of the extensions implied by `denote-file-type'."
(should (let* ((tmp (temporary-file-directory))
(denote-directory tmp)
(file (concat tmp "20230522T154900--test__keyword.txt")))
(with-current-buffer (find-file-noselect file)
(write-file file))
(prog1
(denote-file-is-note-p file)
(delete-file file)))))
(ert-deftest dt-denote-file-has-identifier-p ()
(ert-deftest denote-test--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")))
(should (null (denote-file-has-identifier-p "T154900--test__keyword.txt"))))
(ert-deftest dt-denote-file-has-signature-p ()
(ert-deftest denote-test--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")))
(should (null (denote-file-has-signature-p "20230522T154900--test__keyword.txt"))))
(ert-deftest dt-denote-file-has-supported-extension-p ()
(ert-deftest denote-test--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))))
(should
(member
(file-name-extension "20230522T154900==sig--test__keyword.txt" :period)
(denote-file-type-extensions-with-encryption)))
(should
(null
(member
(file-name-extension "20230522T154900==sig--test__keyword" :period)
(denote-file-type-extensions-with-encryption)))))
(ert-deftest dt-denote-file-type-extensions ()
(ert-deftest denote-test--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."
We check for the common file type extensions, though the user can
theoretically set `denote-file-types' to nil and handle things on
their own. We do not have to test for that scenario, because
such a user will be redefining large parts of Denote's behaviour
with regard to file types."
(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 ()
(ert-deftest denote-test--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)))
@ -212,272 +167,371 @@ Extend what we do in `denote-test--denote-file-type-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 denote-test--denote-surround-with-quotes ()
"Test that `denote-surround-with-quotes' returns a string in quotes."
(should (and (equal (denote-surround-with-quotes "test") "\"test\"")
(equal (denote-surround-with-quotes "") "\"\"")
(equal (denote-surround-with-quotes nil) "\"\"")
(equal (denote-surround-with-quotes 'wrong) "\"\"")
(equal (denote-surround-with-quotes '(wrong)) "\"\""))))
(ert-deftest dt-denote-format-file-name ()
(ert-deftest denote-test--denote--format-front-matter ()
"Test that `denote--format-front-matter' formats front matter correctly."
(should (and (equal (denote--format-front-matter "" "" '("") "" 'text)
(mapconcat #'identity
'("title: "
"date: "
"tags: "
"identifier: "
"---------------------------\n\n")
"\n"))
(equal
(denote--format-front-matter
"Some test" "2023-06-05" '("one" "two")
"20230605T102234" 'text)
(mapconcat #'identity
'("title: Some test"
"date: 2023-06-05"
"tags: one two"
"identifier: 20230605T102234"
"---------------------------\n\n")
"\n"))))
(should (and (equal (denote--format-front-matter "" "" nil "" 'org)
(mapconcat #'identity
'("#+title: "
"#+date: "
"#+filetags: "
"#+identifier: "
"\n")
"\n"))
(equal
(denote--format-front-matter
"Some test" "2023-06-05" '("one" "two")
"20230605T102234" 'org)
(mapconcat #'identity
'("#+title: Some test"
"#+date: 2023-06-05"
"#+filetags: :one:two:"
"#+identifier: 20230605T102234"
"\n")
"\n"))))
(should (and (equal (denote--format-front-matter "" "" nil "" 'markdown-yaml)
(mapconcat #'identity
'("---"
"title: \"\""
"date: "
"tags: []"
"identifier: \"\""
"---"
"\n")
"\n"))
(equal
(denote--format-front-matter
"Some test" "2023-06-05" '("one" "two")
"20230605T102234" 'markdown-yaml)
(mapconcat #'identity
'("---"
"title: \"Some test\""
"date: 2023-06-05"
"tags: [\"one\", \"two\"]"
"identifier: \"20230605T102234\""
"---"
"\n")
"\n"))))
(should (and (equal (denote--format-front-matter "" "" nil "" 'markdown-toml)
(mapconcat #'identity
'("+++"
"title = \"\""
"date = "
"tags = []"
"identifier = \"\""
"+++"
"\n")
"\n"))
(equal
(denote--format-front-matter
"Some test" "2023-06-05" '("one" "two")
"20230605T102234" 'markdown-toml)
(mapconcat #'identity
'("+++"
"title = \"Some test\""
"date = 2023-06-05"
"tags = [\"one\", \"two\"]"
"identifier = \"20230605T102234\""
"+++"
"\n")
"\n")))))
(ert-deftest denote-test--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")))
(id (format-time-string denote-id-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"))))
(should-error (denote-format-file-name
nil
id
kws
title
(denote--file-extension 'org)
""))
(ert-deftest dt-denote-get-file-extension ()
(should-error (denote-format-file-name
""
id
kws
title
(denote--file-extension 'org)
""))
(should-error (denote-format-file-name
denote-directory ; notice this is the `let' bound value without the suffix
id
kws
title
(denote--file-extension 'org)
""))
(should-error (denote-format-file-name
(denote-directory)
nil
kws
title
(denote--file-extension 'org)
""))
(should-error (denote-format-file-name
(denote-directory)
""
kws
title
(denote--file-extension 'org)
""))
(should-error (denote-format-file-name
(denote-directory)
"0123456"
kws
title
(denote--file-extension 'org)
""))
(should (equal (denote-format-file-name
(denote-directory)
id
kws
title
(denote--file-extension 'org)
"")
"/tmp/test-denote/20231128T055311--some-test__one_two.org"))
(should (equal (denote-format-file-name
(denote-directory)
id
nil
""
(denote--file-extension 'org)
"")
"/tmp/test-denote/20231128T055311.org"))
(should (equal (denote-format-file-name
(denote-directory)
id
nil
nil
(denote--file-extension 'org)
nil)
"/tmp/test-denote/20231128T055311.org"))
(should (equal (denote-format-file-name
(denote-directory)
id
kws
title
(denote--file-extension 'org)
"sig")
"/tmp/test-denote/20231128T055311==sig--some-test__one_two.org"))))
(ert-deftest denote-test--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")))
(should (and (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing") "")
(equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org") ".org")
(equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org.gpg") ".org.gpg")
(equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org.age") ".org.age"))))
(ert-deftest dt-denote-get-file-extension-sans-encryption ()
(ert-deftest denote-test--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")))
(should (and (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing") "")
(equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org") ".org")
(equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org.gpg") ".org")
(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 ()
(ert-deftest denote-test--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")))))
(should (and (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing") nil)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org") 'org)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org.gpg") 'org)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org.age") 'org)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt") 'text)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt.gpg") 'text)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt.age") 'text)
;; NOTE 2023-10-11: It returns `markdown-yaml' as a fallback. In
;; an actual file, it reads the file contents to determine what
;; it is and can return `markdown-toml'. In principle, we should
;; be testing this here, though I prefer to keep things simple.
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md") 'markdown-yaml)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md.gpg") 'markdown-yaml)
(eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md.age") 'markdown-yaml))))
(ert-deftest dt-denote-retrieve-filename-identifier ()
(ert-deftest denote-test--denote-get-identifier ()
"Test that `denote-get-identifier' returns an identifier."
(should (and (equal (denote-get-identifier) (format-time-string denote-id-format (current-time)))
(equal (denote-get-identifier "2024-02-01 10:34") "20240201T103400")
(equal (denote-get-identifier 1705644188) "20240119T080308")
(equal (denote-get-identifier '(26026 4251)) "20240119T080307")))
(should-error (denote-get-identifier "Invalid date")))
(ert-deftest denote-test--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")))
(should (and (null
(denote-retrieve-filename-identifier "/path/to/testing/--this-is-a-test-reordered__denote_testing.org"))
(equal
(denote-retrieve-filename-identifier "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org")
"20240610T194654")
(equal
(denote-retrieve-filename-identifier "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org")
"20240610T194654")
(equal
(denote-retrieve-filename-identifier "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org")
"20240610T194654")
(equal
(denote-retrieve-filename-identifier "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org")
"20240610T194654")
(equal
(denote-retrieve-filename-identifier "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org")
"20240610T194654")
(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 ()
(ert-deftest denote-test--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")))
(should (and (null
(denote-retrieve-filename-title "/path/to/testing/20240610T194654__denote_testing.org"))
(equal
(denote-retrieve-filename-title "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org")
"this-is-a-test-reordered")
(equal
(denote-retrieve-filename-title "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org")
"this-is-a-test-reordered")
(equal
(denote-retrieve-filename-title "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org")
"this-is-a-test-reordered")
(equal
(denote-retrieve-filename-title "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org")
"this-is-a-test-reordered")
(equal
(denote-retrieve-filename-title "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org")
"this-is-a-test-reordered")
(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 ()
(ert-deftest denote-test--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")))
(should (and (null
(denote-retrieve-filename-keywords "/path/to/testing/20240610T194654--this-is-a-test-reordered.org"))
(equal
(denote-retrieve-filename-keywords "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org")
"denote_testing")
(equal
(denote-retrieve-filename-keywords "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org")
"denote_testing")
(equal
(denote-retrieve-filename-keywords "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org")
"denote_testing")
(equal
(denote-retrieve-filename-keywords "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org")
"denote_testing")
(equal
(denote-retrieve-filename-keywords "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org")
"denote_testing")
(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 ()
(ert-deftest denote-test--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")))
(should (and (null
(denote-retrieve-filename-signature "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org"))
(equal
(denote-retrieve-filename-signature "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org")
"signature")
(equal
(denote-retrieve-filename-signature "/path/to/testing/--this-is-a-test-reordered==signature__denote_testing@@20240610T194654.org")
"signature")
(equal
(denote-retrieve-filename-signature "/path/to/testing/__denote_testing--this-is-a-test-reordered==signature@@20240610T194654.org")
"signature")
(equal
(denote-retrieve-filename-signature "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered==signature.org")
"signature")
(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")))
;;;; denote-journal-extras.el
(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")))
(require 'denote-journal-extras)
(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)))
(ert-deftest denote-test--denote-journal-extras-daily--title-format ()
"Make sure that `denote-journal-extras-daily--title-format' yields the desired format."
(should (and
;; These three should prompt, but I am here treating the
;; prompt as if already returned a string. The test for
;; the `denote-title-prompt' can be separate.
(stringp
(cl-letf (((symbol-function 'denote-title-prompt) #'identity)
(denote-journal-extras-title-format nil))
(denote-journal-extras-daily--title-format)))
;; 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/"))))
(stringp
(cl-letf (((symbol-function 'denote-title-prompt) #'identity)
(denote-journal-extras-title-format t))
(denote-journal-extras-daily--title-format)))
(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))))
(stringp
(cl-letf (((symbol-function 'denote-title-prompt) #'identity)
(denote-journal-extras-title-format :some-arbitrary-keyword))
(denote-journal-extras-daily--title-format)))
(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")))))
;; And these return the following values
(string-match-p
"\\<.*?\\>"
(let ((denote-journal-extras-title-format 'day))
(denote-journal-extras-daily--title-format)))
(string-match-p
"\\<.*?\\> [0-9]\\{,2\\} \\<.*?\\> [0-9]\\{,4\\}"
(let ((denote-journal-extras-title-format 'day-date-month-year))
(denote-journal-extras-daily--title-format)))
(string-match-p
"\\<.*?\\> [0-9]\\{,2\\} \\<.*?\\> [0-9]\\{,4\\} [0-9]\\{,2\\}:[0-9]\\{,2\\} \\<.*?\\>"
(let ((denote-journal-extras-title-format 'day-date-month-year-12h))
(denote-journal-extras-daily--title-format)))
(string-match-p
"\\<.*?\\> [0-9]\\{,2\\} \\<.*?\\> [0-9]\\{,4\\} [0-9]\\{,2\\}:[0-9]\\{,2\\}"
(let ((denote-journal-extras-title-format 'day-date-month-year-24h))
(denote-journal-extras-daily--title-format))))))
(provide 'denote-test)
;;; denote-test.el ends here
;; Local Variables:
;; read-symbol-shorthands: (("dt" . "denote-test-"))
;; End: