mirror of
https://github.com/protesilaos/denote.git
synced 2026-09-16 02:06:23 -04:00
Compare commits
No commits in common. "main" and "0.3.0" have entirely different histories.
6474
CHANGELOG.org
6474
CHANGELOG.org
File diff suppressed because it is too large
Load diff
4
COPYING
4
COPYING
|
|
@ -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.
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -1,29 +0,0 @@
|
|||
# denote: Simple notes with an efficient file-naming scheme
|
||||
|
||||
New to Denote? Start reading this from the manual:
|
||||
<https://protesilaos.com/emacs/denote#h:c54bedb4-5377-4dbd-853c-5870ace6eb33>.
|
||||
|
||||
* * *
|
||||
|
||||
Denote is a simple note-taking tool for Emacs. It is based on the idea
|
||||
that notes should follow a predictable and descriptive file-naming
|
||||
scheme. The file name must offer a clear indication of what the note is
|
||||
about, without reference to any other metadata. Denote basically
|
||||
streamlines the creation of such files while providing facilities to
|
||||
link between them.
|
||||
|
||||
Denote's file-naming scheme is not limited to "notes". It can be used
|
||||
for all types of file, including those that are not editable in Emacs,
|
||||
such as videos. Naming files in a consistent way makes their
|
||||
filtering and retrieval considerably easier. Denote provides relevant
|
||||
facilities to rename files, regardless of file type.
|
||||
|
||||
+ Package name (GNU ELPA): `denote`
|
||||
+ Official manual: <https://protesilaos.com/emacs/denote>
|
||||
+ Change log: <https://protesilaos.com/emacs/denote-changelog>
|
||||
+ Git repositories:
|
||||
+ GitHub: <https://github.com/protesilaos/denote>
|
||||
+ GitLab: <https://gitlab.com/protesilaos/denote>
|
||||
+ Video demo: <https://protesilaos.com/codelog/2022-06-18-denote-demo/>
|
||||
+ Backronyms: Denote Everything Neatly; Omit The Excesses. Don't Ever
|
||||
Note Only The Epiphenomenal.
|
||||
6691
README.org
6691
README.org
File diff suppressed because it is too large
Load diff
388
denote-dired.el
Normal file
388
denote-dired.el
Normal file
|
|
@ -0,0 +1,388 @@
|
|||
;;; denote-dired.el --- Integration between Denote and Dired -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2022 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Protesilaos Stavrou <info@protesilaos.com>
|
||||
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
|
||||
;; URL: https://git.sr.ht/~protesilaos/denote
|
||||
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
|
||||
;; Version: 0.3.0
|
||||
;; Package-Requires: ((emacs "27.2"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; Denote's file-naming scheme is not specific to notes or text files:
|
||||
;; it is useful for all sorts of files, such as multimedia and PDFs that
|
||||
;; form part of the user's longer-term storage (read manual's "The
|
||||
;; file-naming scheme"). While Denote does not manage such files, it
|
||||
;; already has all the mechanisms to facilitate the task of renaming
|
||||
;; them.
|
||||
;;
|
||||
;; To this end, we provide the `denote-dired-rename-file' command. It
|
||||
;; has a two-fold purpose: (i) to change the name of an existing file
|
||||
;; while retaining its identifier and (ii) to write a Denote-compliant
|
||||
;; file name for an item that was not created by `denote' or related
|
||||
;; commands (such as an image or PDF).
|
||||
;;
|
||||
;; The `denote-dired-rename-file' command will target the file at point
|
||||
;; if it finds one in the current Dired buffer. Otherwise it prompts
|
||||
;; with minibuffer completion for a file name. It then uses the
|
||||
;; familiar prompts for a `TITLE' and `KEYWORDS' the same way the
|
||||
;; `denote' command does (read manual's "Points of entry). As a final
|
||||
;; step, it asks for confirmation before renaming the file at point,
|
||||
;; showing a message like:
|
||||
;;
|
||||
;; Rename sample.pdf to 20220612T052900--my-sample-title__testing.pdf? (y or n)
|
||||
;;
|
||||
;; However, if the user option `denote-dired-rename-expert' is non-nil,
|
||||
;; conduct the renaming operation outright---no questions asked.
|
||||
;;
|
||||
;; When operating on a file that has no identifier, such as
|
||||
;; `sample.pdf', Denote reads the file properties to retrieve its last
|
||||
;; modification time. If the file was from a past date like 2000-11-31
|
||||
;; it will get an identifier starting with `20001131' followed by the
|
||||
;; time component (per our file-naming scheme).
|
||||
;;
|
||||
;; The file type extension (e.g. `.pdf') is read from the underlying
|
||||
;; file and is preserved through the renaming process. Files that have
|
||||
;; no extension are simply left without one.
|
||||
;;
|
||||
;; Renaming only occurs relative to the current directory. Files are not
|
||||
;; moved between directories.
|
||||
;;
|
||||
;; The final step of the `denote-dired-rename-file' command is to call
|
||||
;; the special hook `denote-dired-post-rename-functions'. Functions
|
||||
;; added to that hook must accept three arguments, as explained in its
|
||||
;; doc string. For the time being, the only function we define is the
|
||||
;; one which updates the underlying note's front matter to match the new
|
||||
;; file name: `denote-dired-rewrite-front-matter'. The function takes
|
||||
;; care to only operate on an actual note, instead of arbitrary files.
|
||||
;;
|
||||
;; DEVELOPMENT NOTE: the `denote-dired-rewrite-front-matter' needs to be
|
||||
;; tested thoroughly. It rewrites file contents so we have to be sure
|
||||
;; it does the right thing. To avoid any trouble, it always asks for
|
||||
;; confirmation before performing the replacement. This confirmation
|
||||
;; ignores `denote-dired-rename-expert' for the time being, though we
|
||||
;; might want to lift that restriction once everything works as
|
||||
;; intended.
|
||||
;;
|
||||
;;
|
||||
;; One of the upsides of Denote's file-naming scheme is the predictable
|
||||
;; pattern it establishes, which appears as a near-tabular presentation in
|
||||
;; a listing of notes (i.e. in Dired). The `denote-dired-mode' can help
|
||||
;; enhance this impression, by fontifying the components of the file name
|
||||
;; to make the date (identifier) and keywords stand out.
|
||||
;;
|
||||
;; There are two ways to set the mode. Either use it for all directories,
|
||||
;; which probably is not needed:
|
||||
;;
|
||||
;; (require 'denote-dired)
|
||||
;; (add-hook 'dired-mode-hook #'denote-dired-mode)
|
||||
;;
|
||||
;; Or configure the user option `denote-dired-directories' and then set up
|
||||
;; the function `denote-dired-mode-in-directories':
|
||||
;;
|
||||
;; (require 'denote-dired)
|
||||
;;
|
||||
;; ;; We use different ways to specify a path for demo purposes.
|
||||
;; (setq denote-dired-directories
|
||||
;; (list denote-directory
|
||||
;; (thread-last denote-directory (expand-file-name "attachments"))
|
||||
;; (expand-file-name "~/Documents/vlog")))
|
||||
;;
|
||||
;; (add-hook 'dired-mode-hook #'denote-dired-mode-in-directories)
|
||||
;;
|
||||
;; The `denote-dired-mode' does not only fontify note files that were
|
||||
;; created by Denote: it covers every file name that follows our naming
|
||||
;; conventions (read about "The file-naming scheme" in the manual).
|
||||
;; This is particularly useful for scenaria where, say, one wants to
|
||||
;; organise their collection of PDFs and multimedia in a systematic way
|
||||
;; (and, perhaps, use them as attachments for the notes Denote
|
||||
;; produces).
|
||||
;;
|
||||
;; For the time being, the `diredfl' package is not compatible with this
|
||||
;; facility.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'denote-retrieve)
|
||||
(require 'dired)
|
||||
|
||||
(defgroup denote-dired ()
|
||||
"Integration between Denote and Dired."
|
||||
:group 'denote)
|
||||
|
||||
(defcustom denote-dired-directories
|
||||
;; We use different ways to specify a path for demo purposes.
|
||||
(list denote-directory
|
||||
;; (thread-last denote-directory (expand-file-name "attachments"))
|
||||
(expand-file-name "~/Documents/vlog"))
|
||||
"List of directories where `denote-dired-mode' should apply to."
|
||||
:type '(repeat directory)
|
||||
:group 'denote-dired)
|
||||
|
||||
(defcustom denote-dired-rename-expert nil
|
||||
"If t, `denote-dired-rename-file' doesn't ask for confirmation.
|
||||
The confiration is asked via a `y-or-n-p' prompt which shows the
|
||||
old name followed by the new one."
|
||||
:type 'boolean
|
||||
:group 'denote-dired)
|
||||
|
||||
(defcustom denote-dired-post-rename-functions
|
||||
(list #'denote-dired-update-dired-buffers
|
||||
#'denote-dired-rewrite-front-matter)
|
||||
"List of functions called after `denote-dired-rename-file'.
|
||||
Each function must accept three arguments: FILE, TITLE, and
|
||||
KEYWORDS. The first is the full path to the file provided as a
|
||||
string, the second is the human-readable file name (not what
|
||||
Denote sluggifies) also as a string, and the third are the
|
||||
keywords. If there is only one keyword, it is a string, else a
|
||||
list of strings.
|
||||
|
||||
DEVELOPMENT NOTE: the `denote-dired-rewrite-front-matter' needs
|
||||
to be tested thoroughly. It rewrites file contents so we have to
|
||||
be sure it does the right thing. To avoid any trouble, it always
|
||||
asks for confirmation before performing the replacement. This
|
||||
confirmation ignores `denote-dired-rename-expert' for the time
|
||||
being, though we might want to lift that restriction once
|
||||
everything works as intended."
|
||||
:type 'hook
|
||||
:group 'denote-dired)
|
||||
|
||||
;;;; Commands
|
||||
|
||||
(defun denote-dired--file-attributes-time (file)
|
||||
"Return `file-attribute-modification-time' of FILE as identifier."
|
||||
(format-time-string
|
||||
denote--id-format
|
||||
(file-attribute-modification-time (file-attributes file))))
|
||||
|
||||
(defun denote-dired--file-name-id (file)
|
||||
"Return FILE identifier, else generate one."
|
||||
(cond
|
||||
((string-match denote--id-regexp file)
|
||||
(substring file (match-beginning 0) (match-end 0)))
|
||||
((denote-dired--file-attributes-time file))
|
||||
(t (format-time-string denote--id-format))))
|
||||
|
||||
(defun denote-dired--rename-buffer (old-name new-name)
|
||||
"Rename OLD-NAME buffer to NEW-NAME, when appropriate."
|
||||
(when-let* ((buffer (find-buffer-visiting old-name)))
|
||||
(with-current-buffer buffer
|
||||
(set-visited-file-name new-name nil t))))
|
||||
|
||||
(defun denote-dired--rename-dired-file-or-prompt ()
|
||||
"Return Dired file at point, else prompt for one."
|
||||
(or (dired-get-filename nil t)
|
||||
(let* ((file (buffer-file-name))
|
||||
(format (if file
|
||||
(format "Rename file Denote-style [%s]: " file)
|
||||
"Rename file Denote-style: ")))
|
||||
(read-file-name format nil file t nil))))
|
||||
|
||||
(defun denote-dired--rename-file-is-regular (file)
|
||||
"Throw error is FILE is not regular, else return FILE."
|
||||
(if (or (file-directory-p file)
|
||||
(not (file-regular-p file)))
|
||||
(user-error "Only rename regular files")
|
||||
file))
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-dired-rename-file (file title keywords)
|
||||
"Rename FILE to include TITLE and KEYWORDS.
|
||||
|
||||
If in Dired, consider FILE to be the one at point, else prompt
|
||||
with completion.
|
||||
|
||||
If FILE has a Denote-compliant identifier, retain it while
|
||||
updating the TITLE and KEYWORDS fields of the file name. Else
|
||||
create an identifier based on the file's attribute of last
|
||||
modification time. If such attribute cannot be found, the
|
||||
identifier falls back to the current time.
|
||||
|
||||
As a final step, prompt for confirmation, showing the difference
|
||||
between old and new file names. If `denote-dired-rename-expert'
|
||||
is non-nil, conduct the renaming operation outright---no
|
||||
questions asked!
|
||||
|
||||
The file type extension (e.g. .pdf) is read from the underlying
|
||||
file and is preserved through the renaming process. Files that
|
||||
have no extension are simply left without one.
|
||||
|
||||
Renaming only occurs relative to the current directory. Files
|
||||
are not moved between directories. As a final step, call the
|
||||
`denote-dired-post-rename-functions'.
|
||||
|
||||
This command is intended to (i) rename existing Denote
|
||||
notes, (ii) complement note-taking, such as by renaming
|
||||
attachments that the user adds to their notes."
|
||||
(interactive
|
||||
(let ((file (denote-dired--rename-file-is-regular (denote-dired--rename-dired-file-or-prompt))))
|
||||
(list
|
||||
file
|
||||
(denote--title-prompt (denote-retrieve--value-title file))
|
||||
(denote--keywords-prompt))))
|
||||
(let* ((dir (file-name-directory file))
|
||||
(old-name (file-name-nondirectory file))
|
||||
(extension (file-name-extension file t))
|
||||
(new-name (denote--format-file
|
||||
dir
|
||||
(denote-dired--file-name-id file)
|
||||
keywords
|
||||
(denote--sluggify title)
|
||||
extension))
|
||||
(max-mini-window-height 0.33)) ; allow minibuffer to be resized
|
||||
(unless (string= old-name (file-name-nondirectory new-name))
|
||||
(when (y-or-n-p
|
||||
(format "Rename %s to %s?"
|
||||
(propertize old-name 'face 'error)
|
||||
(propertize (file-name-nondirectory new-name) 'face 'success)))
|
||||
(rename-file old-name new-name nil)
|
||||
(denote-dired--rename-buffer old-name new-name)
|
||||
(run-hook-with-args 'denote-dired-post-rename-functions new-name title keywords)))))
|
||||
|
||||
(defun denote-dired-update-dired-buffers (&rest _)
|
||||
"Update Dired buffers of variable `denote-directory'.
|
||||
Can run after `denote-dired-post-rename-functions', though it
|
||||
ignores all its arguments."
|
||||
(mapc
|
||||
(lambda (buf)
|
||||
(with-current-buffer buf
|
||||
(when (and (eq major-mode 'dired-mode)
|
||||
(string-match-p (expand-file-name default-directory)
|
||||
(expand-file-name (denote-directory))))
|
||||
(revert-buffer))))
|
||||
(buffer-list)))
|
||||
|
||||
(defun denote-dired--file-meta-header (title date keywords id filetype)
|
||||
"Front matter for renamed notes.
|
||||
|
||||
TITLE, DATE, KEYWORDS, FILENAME, ID, and FILETYPE are all strings
|
||||
which are provided by `denote-dired-rewrite-front-matter'."
|
||||
(let ((kw-space (denote--file-meta-keywords keywords))
|
||||
(kw-toml (denote--file-meta-keywords keywords 'toml)))
|
||||
(pcase filetype
|
||||
('markdown-toml (format denote-toml-front-matter title date kw-toml id))
|
||||
('markdown-yaml (format denote-yaml-front-matter title date kw-space id))
|
||||
('text (format denote-text-front-matter title date kw-space id denote-text-front-matter-delimiter))
|
||||
(_ (format denote-org-front-matter title date kw-space id)))))
|
||||
|
||||
(defun denote-dired--filetype-heuristics (file)
|
||||
"Return likely file type of FILE.
|
||||
The return value is for `denote--file-meta-header'."
|
||||
(pcase (file-name-extension file)
|
||||
("md" (if (string-match-p "title\\s-*=" (denote-retrieve--value-title file t))
|
||||
'markdown-toml
|
||||
'markdown-yaml))
|
||||
("txt" 'text)
|
||||
(_ 'org)))
|
||||
|
||||
(defun denote-dired--front-matter-search-delimiter (filetype)
|
||||
"Return likely front matter delimiter search for FILETYPE."
|
||||
(pcase filetype
|
||||
('markdown-toml (re-search-forward "^\\+\\+\\+$" nil t 2))
|
||||
('markdown-yaml (re-search-forward "^---$" nil t 2))
|
||||
;; 2 at most, as the user might prepend it to the block as well.
|
||||
;; Though this might give us false positives, it ultimately is the
|
||||
;; user's fault.
|
||||
('text (or (re-search-forward denote-text-front-matter-delimiter nil t 2)
|
||||
(re-search-forward denote-text-front-matter-delimiter nil t 1)
|
||||
(re-search-forward "^[\s\t]*$" nil t 1)))
|
||||
;; Org does not have a real delimiter. This is the trickiest one.
|
||||
(_ (re-search-forward "^[\s\t]*$" nil t 1))))
|
||||
|
||||
(defun denote-dired--edit-front-matter-p (file)
|
||||
"Test if FILE should be subject to front matter rewrite.
|
||||
This is relevant for `denote-dired-rewrite-front-matter': if FILE
|
||||
has no front matter, then we abort early instead of trying to
|
||||
replace what isn't there."
|
||||
(when-let ((ext (file-name-extension file)))
|
||||
(and (file-regular-p file)
|
||||
(file-writable-p file)
|
||||
(not (denote--file-empty-p file))
|
||||
(string-match-p "\\(md\\|org\\|txt\\)\\'" ext)
|
||||
;; Heuristic to check if this is one of our notes
|
||||
(string= (expand-file-name default-directory) (denote-directory)))))
|
||||
|
||||
(defun denote-dired-rewrite-front-matter (file title keywords)
|
||||
"Rewrite front matter of note after `denote-dired-rename-file'.
|
||||
The FILE, TITLE, and KEYWORDS are passed from the renaming
|
||||
command and are used to construct a new front matter block if
|
||||
appropriate."
|
||||
(when-let* ((denote-dired--edit-front-matter-p file)
|
||||
(id (denote-retrieve--filename-identifier file))
|
||||
(date (denote-retrieve--value-date file))
|
||||
(filetype (denote-dired--filetype-heuristics file))
|
||||
(new-front-matter (denote--file-meta-header title date keywords id filetype)))
|
||||
(let (old-front-matter front-matter-delimiter)
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(widen)
|
||||
(goto-char (point-min))
|
||||
(setq front-matter-delimiter (denote-dired--front-matter-search-delimiter filetype))
|
||||
(when front-matter-delimiter
|
||||
(setq old-front-matter
|
||||
(buffer-substring-no-properties
|
||||
(point-min)
|
||||
(progn front-matter-delimiter (point)))))))
|
||||
(when (and old-front-matter
|
||||
(y-or-n-p
|
||||
(format "%s\n%s\nReplace front matter?"
|
||||
(propertize old-front-matter 'face 'error)
|
||||
(propertize new-front-matter 'face 'success))))
|
||||
(delete-region (point-min) front-matter-delimiter)
|
||||
(goto-char (point-min))
|
||||
(insert new-front-matter)
|
||||
;; FIXME 2022-06-16: Instead of `delete-blank-lines', we
|
||||
;; should check if we added any new lines and delete only
|
||||
;; those.
|
||||
(delete-blank-lines))))))
|
||||
|
||||
;;;; Extra fontification
|
||||
|
||||
(require 'denote-faces)
|
||||
|
||||
;;;###autoload
|
||||
(define-minor-mode denote-dired-mode
|
||||
"Fontify all Denote-style file names in Dired."
|
||||
:global nil
|
||||
:group 'denote-dired
|
||||
(if denote-dired-mode
|
||||
(font-lock-add-keywords nil denote-faces-file-name-keywords t)
|
||||
(font-lock-remove-keywords nil denote-faces-file-name-keywords))
|
||||
(font-lock-flush (point-min) (point-max)))
|
||||
|
||||
(defun denote-dired--modes-dirs-as-dirs ()
|
||||
"Return `denote-dired-directories' as directories.
|
||||
The intent is to basically make sure that however a path is
|
||||
written, it is always returned as a directory."
|
||||
(mapcar
|
||||
(lambda (dir)
|
||||
(file-name-as-directory (file-truename dir)))
|
||||
denote-dired-directories))
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-dired-mode-in-directories ()
|
||||
"Enable `denote-dired-mode' in `denote-dired-directories'.
|
||||
Add this function to `dired-mode-hook'."
|
||||
(when (member (file-truename default-directory) (denote-dired--modes-dirs-as-dirs))
|
||||
(denote-dired-mode 1)))
|
||||
|
||||
(provide 'denote-dired)
|
||||
;;; denote-dired.el ends here
|
||||
121
denote-faces.el
Normal file
121
denote-faces.el
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
;;; denote-faces.el --- Faces and fontification rules for Denote -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2022 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Protesilaos Stavrou <info@protesilaos.com>
|
||||
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
|
||||
;; URL: https://git.sr.ht/~protesilaos/denote
|
||||
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
|
||||
;; Version: 0.3.0
|
||||
;; Package-Requires: ((emacs "27.2"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; Used internally by Denote to fontify file names in Dired, the
|
||||
;; backlinks' buffer, and related.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'denote)
|
||||
|
||||
(defgroup denote-faces ()
|
||||
"Faces for Denote."
|
||||
:group 'denote)
|
||||
|
||||
(defface denote-faces-subdirectory
|
||||
'((t :inherit bold))
|
||||
"Face for subdirectory of file name.
|
||||
This should only ever needed in the backlinks' buffer (or
|
||||
equivalent), not in Dired."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defface denote-faces-date
|
||||
'((t :inherit font-lock-variable-name-face))
|
||||
"Face for file name date in Dired buffers.
|
||||
This is the part of the identifier that covers the year, month,
|
||||
and day."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defface denote-faces-time
|
||||
'((t :inherit denote-faces-date))
|
||||
"Face for file name time in Dired buffers.
|
||||
This is the part of the identifier that covers the hours, minutes,
|
||||
and seconds."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defface denote-faces-title
|
||||
'((t ))
|
||||
"Face for file name title in Dired buffers."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defface denote-faces-extension
|
||||
'((t :inherit shadow))
|
||||
"Face for file extension type in Dired buffers."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defface denote-faces-keywords
|
||||
'((t :inherit font-lock-builtin-face))
|
||||
"Face for file name keywords in Dired buffers."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defface denote-faces-delimiter
|
||||
'((((class color) (min-colors 88) (background light))
|
||||
:foreground "gray70")
|
||||
(((class color) (min-colors 88) (background dark))
|
||||
:foreground "gray30")
|
||||
(t :inherit shadow))
|
||||
"Face for file name delimiters in Dired buffers."
|
||||
:group 'denote-faces)
|
||||
|
||||
(defvar denote-faces--file-name-regexp
|
||||
(concat "\\(?1:[0-9]\\{8\\}\\)\\(?2:T[0-9]\\{6\\}\\)"
|
||||
"\\(?:\\(?3:--\\)\\(?4:[0-9A-Za-z-]*\\)\\)?"
|
||||
"\\(?:\\(?5:__\\)\\(?6:[0-9A-Za-z_-]*\\)\\)?"
|
||||
"\\(?7:\\..*\\)?$")
|
||||
"Regexp of file names for fontification.")
|
||||
|
||||
(defconst denote-faces-file-name-keywords
|
||||
`((,(concat " " denote-faces--file-name-regexp)
|
||||
(1 'denote-faces-date)
|
||||
(2 'denote-faces-time)
|
||||
(3 'denote-faces-delimiter nil t)
|
||||
(4 'denote-faces-title nil t)
|
||||
(5 'denote-faces-delimiter nil t)
|
||||
(6 'denote-faces-keywords nil t)
|
||||
(7 'denote-faces-extension nil t )))
|
||||
"Keywords for fontification of file names.")
|
||||
|
||||
(define-obsolete-variable-alias
|
||||
'denote-faces-file-name-with-subdir-keywords
|
||||
'denote-faces-file-name-keywords-for-backlinks
|
||||
"0.3.0")
|
||||
|
||||
(defconst denote-faces-file-name-keywords-for-backlinks
|
||||
`((,(concat "^\\(?8:.*/\\)?" denote-faces--file-name-regexp)
|
||||
(8 'denote-faces-subdirectory nil t)
|
||||
(1 'denote-faces-date)
|
||||
(2 'denote-faces-time)
|
||||
(3 'denote-faces-delimiter nil t)
|
||||
(4 'denote-faces-title nil t)
|
||||
(5 'denote-faces-delimiter nil t)
|
||||
(6 'denote-faces-keywords nil t)
|
||||
(7 'denote-faces-extension nil t )))
|
||||
"Keywords for fontification of file names in the backlinks buffer.")
|
||||
|
||||
(provide 'denote-faces)
|
||||
;;; denote-faces.el ends here
|
||||
556
denote-link.el
Normal file
556
denote-link.el
Normal file
|
|
@ -0,0 +1,556 @@
|
|||
;;; denote-link.el --- Link facility for Denote -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2022 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Protesilaos Stavrou <info@protesilaos.com>
|
||||
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
|
||||
;; URL: https://git.sr.ht/~protesilaos/denote
|
||||
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
|
||||
;; Version: 0.3.0
|
||||
;; Package-Requires: ((emacs "27.2"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; The `denote-link' command inserts a link at point to an entry specified
|
||||
;; at the minibuffer prompt. Links are formatted depending on the file
|
||||
;; type of current note. In Org and plain text buffers, links are
|
||||
;; formatted thus: `[[denote:IDENTIFIER][TITLE]]'. While in Markdown they
|
||||
;; are expressed as `[TITLE](denote:IDENTIFIER)'.
|
||||
;;
|
||||
;; When `denote-link' is called with a prefix argument (`C-u' by default),
|
||||
;; it formats links like `[[denote:IDENTIFIER]]'. The user might prefer
|
||||
;; its simplicity.
|
||||
;;
|
||||
;; Inserted links are automatically buttonized and remain active for as
|
||||
;; long as the buffer is available. In Org this is handled by the major
|
||||
;; mode: the `denote:' hyperlink type works exactly like the standard
|
||||
;; `file:'. In Markdown and plain text, Denote performs the buttonization
|
||||
;; of those links. To buttonize links in existing files while visiting
|
||||
;; them, the user must add this snippet to their setup (it already excludes
|
||||
;; Org):
|
||||
;;
|
||||
;; (add-hook 'find-file-hook #'denote-link-buttonize-buffer)
|
||||
;;
|
||||
;; Denote has a major-mode-agnostic mechanism to collect all linked file
|
||||
;; references in the current buffer and return them as an appropriately
|
||||
;; formatted list. This list can then be used in interactive commands.
|
||||
;; The `denote-link-find-file' is such a command. It uses minibuffer
|
||||
;; completion to visit a file that is linked to from the current note.
|
||||
;; The candidates have the correct metadata, which is ideal for
|
||||
;; integration with other standards-compliant tools (see manual's
|
||||
;; "Extending Denote"). For instance, a package such as `marginalia'
|
||||
;; will display accurate annotations, while the `embark' package will be
|
||||
;; able to work its magic such as in exporting the list into a filtered
|
||||
;; Dired buffer (i.e. a familiar Dired listing with only the files of
|
||||
;; the current minibuffer session).
|
||||
;;
|
||||
;; The command `denote-link-backlinks' produces a bespoke buffer which
|
||||
;; displays the file name of all notes linking to the current one. Each
|
||||
;; file name appears on its own line and is buttonized so that it performs
|
||||
;; the action of visiting the referenced file. The backlinks' buffer looks
|
||||
;; like this:
|
||||
;;
|
||||
;; Backlinks to "On being honest" (20220614T130812)
|
||||
;; ------------------------------------------------
|
||||
;;
|
||||
;; 20220614T145606--let-this-glance-become-a-stare__journal.txt
|
||||
;; 20220616T182958--not-feeling-butterflies-in-your-stomach__journal.txt
|
||||
;;
|
||||
;; The backlinks' buffer is fontified by default, though the user has
|
||||
;; access to the `denote-link-fontify-backlinks' option to disable this
|
||||
;; effect by setting its value to nil.
|
||||
;;
|
||||
;; The placement of the backlinks' buffer is subject to the user option
|
||||
;; `denote-link-backlinks-display-buffer-action'. Due to the nature of the
|
||||
;; underlying `display-buffer' mechanism, this inevitably is a relatively
|
||||
;; advanced feature. By default, the backlinks' buffer is displayed below
|
||||
;; the current window. The doc string of our user option includes a sample
|
||||
;; configuration that places the buffer in a left side window instead.
|
||||
;; Reproducing it here for the sake of convenience:
|
||||
;;
|
||||
;; (setq denote-link-backlinks-display-buffer-action
|
||||
;; '((display-buffer-reuse-window
|
||||
;; display-buffer-in-side-window)
|
||||
;; (side . left)
|
||||
;; (slot . 99)
|
||||
;; (window-width . 0.3)))
|
||||
;;
|
||||
;; The command `denote-link-add-links' adds links at point matching a
|
||||
;; regular expression or plain string. The links are inserted as a
|
||||
;; typographic list, such as:
|
||||
;;
|
||||
;; - link1
|
||||
;; - link2
|
||||
;; - link3
|
||||
;;
|
||||
;; Each link is formatted according to the file type of the current note,
|
||||
;; as explained further above about the `denote-link' command. The current
|
||||
;; note is excluded from the matching entries (adding a link to itself is
|
||||
;; pointless).
|
||||
;;
|
||||
;; When called with a prefix argument (`C-u') `denote-link-add-links' will
|
||||
;; format all links as `[[denote:IDENTIFIER]]', hence a typographic list:
|
||||
;;
|
||||
;; - [[denote:IDENTIFIER-1]]
|
||||
;; - [[denote:IDENTIFIER-2]]
|
||||
;; - [[denote:IDENTIFIER-3]]
|
||||
;;
|
||||
;; Same examples of a regular expression that can be used with this
|
||||
;; command:
|
||||
;;
|
||||
;; - `journal' match all files which include `journal' anywhere in their
|
||||
;; name.
|
||||
;;
|
||||
;; - `_journal' match all files which include `journal' as a keyword.
|
||||
;;
|
||||
;; - `^2022.*_journal' match all file names starting with `2022' and
|
||||
;; including the keyword `journal'.
|
||||
;;
|
||||
;; - `\.txt' match all files including `.txt'. In practical terms, this
|
||||
;; only applies to the file extension, as Denote automatically removes
|
||||
;; dots (and other characters) from the base file name.
|
||||
;;
|
||||
;; If files are created with `denote-sort-keywords' as non-nil (the
|
||||
;; default), then it is easy to write a regexp that includes multiple
|
||||
;; keywords in alphabetic order:
|
||||
;;
|
||||
;; - `_denote.*_package' match all files that include both the `denote' and
|
||||
;; `package' keywords, in this order.
|
||||
;;
|
||||
;; - `\(.*denote.*package.*\)\|\(.*package.*denote.*\)' is the same as
|
||||
;; above, but out-of-order.
|
||||
;;
|
||||
;; Remember that regexp constructs only need to be escaped once (like `\|')
|
||||
;; when done interactively but twice when called from Lisp. What we show
|
||||
;; above is for interactive usage.
|
||||
;;
|
||||
;; For convenience, the `denote-link' command has an alias called
|
||||
;; `denote-link-insert-link'. The `denote-link-backlinks' can also be used
|
||||
;; as `denote-link-show-backlinks-buffer'. While `denote-link-add-links'
|
||||
;; is aliased `denote-link-insert-links-matching-regexp'. The purpose of
|
||||
;; these aliases is to offer alternative, more descriptive names of select
|
||||
;; commands.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'denote-retrieve)
|
||||
|
||||
(defgroup denote-link ()
|
||||
"Link facility for Denote."
|
||||
:group 'denote)
|
||||
|
||||
;;;; User options
|
||||
|
||||
(defcustom denote-link-fontify-backlinks t
|
||||
"When non-nil, apply faces to files in the backlinks' buffer."
|
||||
:type 'boolean
|
||||
:group 'denote-link)
|
||||
|
||||
(defcustom denote-link-backlinks-display-buffer-action
|
||||
'((display-buffer-reuse-window display-buffer-below-selected)
|
||||
(window-height . fit-window-to-buffer))
|
||||
"The action used to display the current file's backlinks buffer.
|
||||
|
||||
The value has the form (FUNCTION . ALIST), where FUNCTION is
|
||||
either an \"action function\", a list thereof, or possibly an
|
||||
empty list. ALIST is a list of \"action alist\" which may be
|
||||
omitted (or be empty).
|
||||
|
||||
Sample configuration to display the buffer in a side window on
|
||||
the left of the Emacs frame:
|
||||
|
||||
(setq denote-link-backlinks-display-buffer-action
|
||||
(quote ((display-buffer-reuse-window
|
||||
display-buffer-in-side-window)
|
||||
(side . left)
|
||||
(slot . 99)
|
||||
(window-width . 0.3))))
|
||||
|
||||
See Info node `(elisp) Displaying Buffers' for more details
|
||||
and/or the documentation string of `display-buffer'."
|
||||
:type '(cons (choice (function :tag "Display Function")
|
||||
(repeat :tag "Display Functions" function))
|
||||
alist)
|
||||
:group 'denote-link)
|
||||
|
||||
;;;; Link to note
|
||||
|
||||
;; Arguments are: FILE-ID FILE-TITLE
|
||||
(defconst denote-link--format-org "[[denote:%s][%s]]"
|
||||
"Format of Org link to note.")
|
||||
|
||||
(defconst denote-link--format-markdown "[%2$s](denote:%1$s)"
|
||||
"Format of Markdown link to note.")
|
||||
|
||||
(defconst denote-link--format-id-only "[[denote:%s]]"
|
||||
"Format of identifier-only link to note.")
|
||||
|
||||
(defconst denote-link--regexp-org
|
||||
(concat "\\[\\[" "denote:" "\\(?1:" denote--id-regexp "\\)" "]" "\\[.*?]]"))
|
||||
|
||||
(defconst denote-link--regexp-markdown
|
||||
(concat "\\[.*?]" "(denote:" "\\(?1:" denote--id-regexp "\\)" ")"))
|
||||
|
||||
(defconst denote-link--regexp-plain
|
||||
(concat "\\[\\[" "denote:" "\\(?1:" denote--id-regexp "\\)" "]]"))
|
||||
|
||||
(defun denote-link--file-type-format (current-file id-only)
|
||||
"Return link format based on CURRENT-FILE format.
|
||||
With non-nil ID-ONLY, use the generic link format without a
|
||||
title."
|
||||
;; Includes backup files. Maybe we can remove them?
|
||||
(let ((current-file-ext (file-name-extension current-file)))
|
||||
(cond
|
||||
(id-only denote-link--format-id-only)
|
||||
((string= current-file-ext "md")
|
||||
denote-link--format-markdown)
|
||||
;; Plain text also uses [[denote:ID][TITLE]]
|
||||
(t denote-link--format-org))))
|
||||
|
||||
(defun denote-link--file-type-regexp (file)
|
||||
"Return link regexp based on FILE format."
|
||||
(pcase (file-name-extension file)
|
||||
("md" denote-link--regexp-markdown)
|
||||
(_ denote-link--regexp-org)))
|
||||
|
||||
(defun denote-link--format-link (file pattern)
|
||||
"Prepare link to FILE using PATTERN."
|
||||
(let ((file-id (denote-retrieve--filename-identifier file))
|
||||
(file-title (unless (string= pattern denote-link--format-id-only)
|
||||
(denote-retrieve--value-title file))))
|
||||
(format pattern file-id file-title)))
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-link (target &optional id-only)
|
||||
"Create link to TARGET note in variable `denote-directory'.
|
||||
With optional ID-ONLY, such as a universal prefix
|
||||
argument (\\[universal-argument]), insert links with just the
|
||||
identifier and no further description. In this case, the link
|
||||
format is always [[denote:IDENTIFIER]]."
|
||||
(interactive (list (denote-retrieve--read-file-prompt) current-prefix-arg))
|
||||
(let ((beg (point)))
|
||||
(insert
|
||||
(denote-link--format-link
|
||||
target
|
||||
(denote-link--file-type-format (buffer-file-name) id-only)))
|
||||
(unless (derived-mode-p 'org-mode)
|
||||
(make-button beg (point) 'type 'denote-link-button))))
|
||||
|
||||
(defalias 'denote-link-insert-link (symbol-function 'denote-link))
|
||||
|
||||
(defun denote-link--collect-identifiers (regexp)
|
||||
"Return collection of identifiers in buffer matching REGEXP."
|
||||
(let (matches)
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(while (re-search-forward regexp nil t)
|
||||
(push (match-string-no-properties 1) matches)))
|
||||
matches))
|
||||
|
||||
(defun denote-link--expand-identifiers (regexp)
|
||||
"Expend identifiers matching REGEXP into file paths."
|
||||
(let ((files (denote--directory-files))
|
||||
(found-files))
|
||||
(dolist (file files)
|
||||
(dolist (i (denote-link--collect-identifiers regexp))
|
||||
(when (string-prefix-p i (file-name-nondirectory file))
|
||||
(push file found-files))))
|
||||
found-files))
|
||||
|
||||
(defvar denote-link--find-file-history nil
|
||||
"History for `denote-link-find-file'.")
|
||||
|
||||
(defun denote-link--find-file-prompt (files)
|
||||
"Prompt for linked file among FILES."
|
||||
(completing-read "Find linked file "
|
||||
(denote--completion-table 'file files)
|
||||
nil t
|
||||
nil 'denote-link--find-file-history))
|
||||
|
||||
;; TODO 2022-06-14: Do we need to add any sort of extension to better
|
||||
;; integrate with Embark? For the minibuffer interaction it is not
|
||||
;; necessary, but maybe it can be done to immediately recognise the
|
||||
;; identifiers are links to files?
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-link-find-file ()
|
||||
"Use minibuffer completion to visit linked file."
|
||||
(interactive)
|
||||
(if-let* ((regexp (denote-link--file-type-regexp (buffer-file-name)))
|
||||
(files (denote-link--expand-identifiers regexp)))
|
||||
(find-file (denote-link--find-file-prompt files))
|
||||
(user-error "No links found in the current buffer")))
|
||||
|
||||
;;;; Link buttons
|
||||
|
||||
;; Evaluate: (info "(elisp) Button Properties")
|
||||
;;
|
||||
;; Button can provide a help-echo function as well, but I think we might
|
||||
;; not need it.
|
||||
(define-button-type 'denote-link-button
|
||||
'follow-link t
|
||||
'action #'denote-link--find-file-at-button)
|
||||
|
||||
(autoload 'thing-at-point-looking-at "thingatpt")
|
||||
|
||||
(defun denote-link--link-at-point-string ()
|
||||
"Return identifier at point."
|
||||
(when (or (thing-at-point-looking-at denote-link--regexp-plain)
|
||||
(thing-at-point-looking-at denote-link--regexp-markdown)
|
||||
(thing-at-point-looking-at denote-link--regexp-org)
|
||||
;; Meant to handle the case where a link is broken by
|
||||
;; `fill-paragraph' into two lines, in which case it
|
||||
;; buttonizes only the "denote:ID" part. Example:
|
||||
;;
|
||||
;; [[denote:20220619T175212][This is a
|
||||
;; test]]
|
||||
;;
|
||||
;; Maybe there is a better way?
|
||||
(thing-at-point-looking-at "\\[\\(denote:.*\\)]"))
|
||||
(match-string-no-properties 0)))
|
||||
|
||||
(defun denote-link--id-from-string (string)
|
||||
"Extract identifier from STRING."
|
||||
(replace-regexp-in-string
|
||||
(concat ".*denote:" "\\(" denote--id-regexp "\\)" ".*")
|
||||
"\\1" string))
|
||||
|
||||
;; NOTE 2022-06-15: I add this as a variable for advanced users who may
|
||||
;; prefer something else. If there is demand for it, we can make it a
|
||||
;; defcustom, but I think it would be premature at this stage.
|
||||
(defvar denote-link-buton-action #'find-file-other-window
|
||||
"Action for Denote buttons.")
|
||||
|
||||
(defun denote-link--find-file-at-button (button)
|
||||
"Visit file referenced by BUTTON."
|
||||
(let* ((id (denote-link--id-from-string
|
||||
(buffer-substring-no-properties
|
||||
(button-start button)
|
||||
(button-end button))))
|
||||
(file (denote--get-note-path-by-id id)))
|
||||
(funcall denote-link-buton-action file)))
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-link-buttonize-buffer (&optional beg end)
|
||||
"Make denote: links actionable buttons in the current buffer.
|
||||
|
||||
Add this to `find-file-hook'. It will only work with Denote
|
||||
notes and will not do anything in `org-mode' buffers, as buttons
|
||||
already work there. If you do not use Markdown or plain text,
|
||||
then you do not need this.
|
||||
|
||||
When called from Lisp, with optional BEG and END as buffer
|
||||
positions, limit the process to the region in-between."
|
||||
(when (and (not (derived-mode-p 'org-mode)) (denote--current-file-is-note-p))
|
||||
(save-excursion
|
||||
(goto-char (or beg (point-min)))
|
||||
(while (re-search-forward denote--id-regexp end t)
|
||||
(when-let ((string (denote-link--link-at-point-string))
|
||||
(beg (match-beginning 0))
|
||||
(end (match-end 0)))
|
||||
(make-button beg end 'type 'denote-link-button))))))
|
||||
|
||||
;;;; Backlinks' buffer
|
||||
|
||||
(define-button-type 'denote-link-backlink-button
|
||||
'follow-link t
|
||||
'action #'denote-link--backlink-find-file
|
||||
'face nil) ; we use this face though we style it later
|
||||
|
||||
(defun denote-link--backlink-find-file (button)
|
||||
"Action for BUTTON to `find-file'."
|
||||
(funcall denote-link-buton-action (buffer-substring (button-start button) (button-end button))))
|
||||
|
||||
(defun denote-link--display-buffer (buf)
|
||||
"Run `display-buffer' on BUF.
|
||||
Expand `denote-link-backlinks-display-buffer-action'."
|
||||
(display-buffer
|
||||
buf
|
||||
`(,@denote-link-backlinks-display-buffer-action)))
|
||||
|
||||
(require 'denote-faces)
|
||||
|
||||
(defun denote-link--prepare-backlinks (id files &optional title)
|
||||
"Create backlinks' buffer for ID including FILES.
|
||||
Use optional TITLE for a prettier heading."
|
||||
(let ((inhibit-read-only t)
|
||||
(buf (format "*denote-backlinks to %s*" id)))
|
||||
(with-current-buffer (get-buffer-create buf)
|
||||
(erase-buffer)
|
||||
(special-mode)
|
||||
(goto-char (point-min))
|
||||
(when-let* ((title)
|
||||
(heading (format "Backlinks to %S (%s)" title id))
|
||||
(l (length heading)))
|
||||
(insert (format "%s\n%s\n\n" heading (make-string l ?-))))
|
||||
(mapc (lambda (f)
|
||||
(insert f)
|
||||
(make-button (point-at-bol) (point-at-eol) :type 'denote-link-backlink-button)
|
||||
(newline))
|
||||
files)
|
||||
(goto-char (point-min))
|
||||
(when denote-link-fontify-backlinks
|
||||
(font-lock-add-keywords nil denote-faces-file-name-keywords-for-backlinks t)))
|
||||
(denote-link--display-buffer buf)))
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-link-backlinks ()
|
||||
"Produce a buffer with files linking to current note.
|
||||
Each file is a clickable/actionable button that visits the
|
||||
referenced entry. Files are fontified if the user option
|
||||
`denote-link-fontify-backlinks' is non-nil.
|
||||
|
||||
The placement of the backlinks' buffer is controlled by the user
|
||||
option `denote-link-backlinks-display-buffer-action'. By
|
||||
default, it will show up below the current window."
|
||||
(interactive)
|
||||
(let* ((default-directory (denote-directory))
|
||||
(file (buffer-file-name))
|
||||
(id (denote-retrieve--filename-identifier file))
|
||||
(title (denote-retrieve--value-title file)))
|
||||
(if-let ((files (denote-retrieve--proces-grep id)))
|
||||
(denote-link--prepare-backlinks id files title)
|
||||
(user-error "No links to the current note"))))
|
||||
|
||||
(defalias 'denote-link-show-backlinks-buffer (symbol-function 'denote-link-backlinks))
|
||||
|
||||
;;;; Add links matching regexp
|
||||
|
||||
(defvar denote-link--links-to-files nil
|
||||
"String of `denote-link-add-links-matching-keyword'.")
|
||||
|
||||
(defvar denote-link--prepare-links-format "- %s\n"
|
||||
"Format specifiers for `denote-link-add-links'.")
|
||||
|
||||
;; NOTE 2022-06-16: There is no need to overwhelm the user with options,
|
||||
;; though I expect someone to want to change the sort order.
|
||||
(defvar denote-link-add-links-sort nil
|
||||
"Add REVERSE to `sort-lines' of `denote-link-add-links' when t.")
|
||||
|
||||
(defun denote-link--prepare-links (files current-file id-only)
|
||||
"Prepare links to FILES from CURRENT-FILE.
|
||||
When ID-ONLY is non-nil, use a generic link format. See
|
||||
`denote-link--file-type-format'."
|
||||
(setq denote-link--links-to-files
|
||||
(with-temp-buffer
|
||||
(mapc (lambda (file)
|
||||
(insert
|
||||
(format
|
||||
denote-link--prepare-links-format
|
||||
(denote-link--format-link
|
||||
file
|
||||
(denote-link--file-type-format current-file id-only)))))
|
||||
files)
|
||||
(sort-lines denote-link-add-links-sort (point-min) (point-max))
|
||||
(buffer-string))))
|
||||
|
||||
(defvar denote-link--add-links-history nil
|
||||
"Minibuffer history for `denote-link-add-links'.")
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-link-add-links (regexp &optional id-only)
|
||||
"Insert links to all notes matching REGEXP.
|
||||
Use this command to reference multiple files at once.
|
||||
Particularly useful for the creation of metanotes (read the
|
||||
manual for more on the matter).
|
||||
|
||||
Optional ID-ONLY has the same meaning as in `denote-link': it
|
||||
inserts links with just the identifier."
|
||||
(interactive
|
||||
(list
|
||||
(read-regexp "Insert links matching REGEX: " nil 'denote-link--add-links-history)
|
||||
current-prefix-arg))
|
||||
(let* ((default-directory (denote-directory))
|
||||
(current-file (buffer-file-name)))
|
||||
(if-let ((files (denote--directory-files-matching-regexp regexp)))
|
||||
(let ((beg (point)))
|
||||
(insert (denote-link--prepare-links files current-file id-only))
|
||||
(unless (derived-mode-p 'org-mode)
|
||||
(denote-link-buttonize-buffer beg (point))))
|
||||
(user-error "No links matching `%s'" regexp))))
|
||||
|
||||
(defalias 'denote-link-insert-links-matching-regexp (symbol-function 'denote-link-add-links))
|
||||
|
||||
;;;; Register `denote:' custom Org hyperlink
|
||||
|
||||
(autoload 'org-link-set-parameters "ol.el")
|
||||
|
||||
(org-link-set-parameters
|
||||
"denote"
|
||||
:follow #'denote-link-ol-follow
|
||||
:complete #'denote-link-ol-complete
|
||||
:export #'denote-link-ol-export)
|
||||
|
||||
(declare-function org-link-open-as-file "ol" (path arg))
|
||||
|
||||
(defun denote-link--ol-resolve-link-to-target (link &optional path-id)
|
||||
"Resolve LINK into the appropriate target.
|
||||
With optional PATH-ID return a cons cell consisting of the path
|
||||
and the identifier."
|
||||
(let* ((search (and (string-match "::\\(.*\\)\\'" link)
|
||||
(match-string 1 link)))
|
||||
(id (if (and (stringp search) (not (string-empty-p search)))
|
||||
(substring link 0 (match-beginning 0))
|
||||
link))
|
||||
(path (denote--get-note-path-by-id id)))
|
||||
(cond
|
||||
(path-id
|
||||
(cons (format "%s" path) (format "%s" id)))
|
||||
((and (stringp search) (not (string-empty-p search)))
|
||||
(concat path "::" search))
|
||||
(path))))
|
||||
|
||||
(defun denote-link-ol-follow (link)
|
||||
"Find file of type `denote:' matching LINK.
|
||||
LINK is the identifier of the note, optionally followed by a
|
||||
search option akin to that of standard Org `file:' link types.
|
||||
Read Info node `(org) Search Options'.
|
||||
|
||||
Uses the function `denote-directory' to establish the path to the
|
||||
file."
|
||||
(org-link-open-as-file
|
||||
(denote-link--ol-resolve-link-to-target link)
|
||||
nil))
|
||||
|
||||
(defun denote-link-ol-complete ()
|
||||
"Like `denote-link' but for Org integration.
|
||||
This lets the user complete a link through the `org-insert-link'
|
||||
interface by first selecting the `denote:' hyperlink type."
|
||||
(concat
|
||||
"denote:"
|
||||
(denote-retrieve--filename-identifier (denote-retrieve--read-file-prompt))))
|
||||
|
||||
(defun denote-link-ol-export (link description format)
|
||||
"Export a `denote:' link from Org files.
|
||||
The LINK, DESCRIPTION, and FORMAT are handled by the export
|
||||
backend."
|
||||
(let* ((path-id (denote-link--ol-resolve-link-to-target link :path-id))
|
||||
(path (file-name-nondirectory (car path-id)))
|
||||
(p (file-name-sans-extension path))
|
||||
(id (cdr path-id))
|
||||
(desc (or description (concat "denote:" id))))
|
||||
(cond
|
||||
((eq format 'html) (format "<a target=\"_blank\" href=\"%s.html\">%s</a>" p desc))
|
||||
((eq format 'latex) (format "\\href{%s}{%s}" (replace-regexp-in-string "[\\{}$%&_#~^]" "\\\\\\&" path) desc))
|
||||
((eq format 'texinfo) (format "@uref{%s,%s}" path desc))
|
||||
((eq format 'ascii) (format "[%s] <denote:%s>" desc path)) ; NOTE 2022-06-16: May be tweaked further
|
||||
((eq format 'md) (format "[%s](%s.md)" desc p))
|
||||
(t path))))
|
||||
|
||||
(provide 'denote-link)
|
||||
;;; denote-link.el ends here
|
||||
106
denote-org-capture.el
Normal file
106
denote-org-capture.el
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
;;; denote-org-capture.el --- Integration between Denote and org-capture -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2022 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Protesilaos Stavrou <info@protesilaos.com>
|
||||
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
|
||||
;; URL: https://git.sr.ht/~protesilaos/denote
|
||||
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
|
||||
;; Version: 0.3.0
|
||||
;; Package-Requires: ((emacs "27.2"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; Denote integration with org-capture.
|
||||
;;
|
||||
;; Samples of an `org-capture-templates' entry:
|
||||
;;
|
||||
;; (setq org-capture-templates
|
||||
;; '(("n" "New note (with denote.el)" plain
|
||||
;; (file denote-last-path)
|
||||
;; #'denote-org-capture
|
||||
;; :no-save t
|
||||
;; :immediate-finish nil
|
||||
;; :kill-buffer t
|
||||
;; :jump-to-captured t)))
|
||||
;;
|
||||
;; (with-eval-after-load 'org-capture
|
||||
;; (add-to-list 'org-capture-templates
|
||||
;; '("n" "New note (with denote.el)" plain
|
||||
;; (file denote-last-path)
|
||||
;; #'denote-org-capture
|
||||
;; :no-save t
|
||||
;; :immediate-finish nil
|
||||
;; :kill-buffer t
|
||||
;; :jump-to-captured t)))
|
||||
;;
|
||||
;; Note that `denote-org-capture' ignores the `denote-file-type': it
|
||||
;; always sets the Org file extension for the created note to ensure
|
||||
;; that the capture process works as intended, especially for the
|
||||
;; desired output of the `denote-org-capture-specifiers'.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'denote)
|
||||
|
||||
(defgroup denote-org-capture ()
|
||||
"Integration between Denote and Org Capture."
|
||||
:group 'denote)
|
||||
|
||||
(defcustom denote-org-capture-specifiers "%l\n%i\n%?"
|
||||
"String with format specifiers for `org-capture-templates'.
|
||||
Check that variable's documentation for the details.
|
||||
|
||||
This string is appended to new notes in the `denote-org-capture'
|
||||
function. Every new note has the standard front matter we
|
||||
define."
|
||||
:type 'string
|
||||
:group 'denote-org-capture)
|
||||
|
||||
;;;###autoload
|
||||
(defun denote-org-capture ()
|
||||
"Create new note through `org-capture-templates'.
|
||||
Use this as a function that returns the path to the new file.
|
||||
The file is populated with Denote's front matter. It can then be
|
||||
expanded with the usual specifiers or strings that
|
||||
`org-capture-templates' supports.
|
||||
|
||||
Note that this function ignores the `denote-file-type': it always
|
||||
sets the Org file extension for the created note to ensure that
|
||||
the capture process works as intended, especially for the desired
|
||||
output of the `denote-org-capture-specifiers'.
|
||||
|
||||
Consult the manual for template samples."
|
||||
(let ((title (denote--title-prompt))
|
||||
(keywords (denote--keywords-prompt))
|
||||
(denote-file-type nil)) ; we enforce the .org extension for `org-capture'
|
||||
(denote--path title keywords)
|
||||
(denote--prepare-note denote-last-title denote-last-keywords denote-last-path)
|
||||
(denote--keywords-add-to-history denote-last-keywords)
|
||||
(concat denote-last-front-matter denote-org-capture-specifiers)))
|
||||
|
||||
(defun denote-org-capture-delete-empty-file ()
|
||||
"Delete file if capture with `denote-org-capture' is aborted."
|
||||
(when-let* ((file denote-last-path)
|
||||
((denote--file-empty-p file)))
|
||||
(delete-file denote-last-path)))
|
||||
|
||||
(add-hook 'org-capture-after-finalize-hook #'denote-org-capture-delete-empty-file)
|
||||
|
||||
(provide 'denote-org-capture)
|
||||
;;; denote-org-capture.el ends here
|
||||
121
denote-retrieve.el
Normal file
121
denote-retrieve.el
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
;;; denote-retrieve.el --- Internal search functions for Denote -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2022 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Protesilaos Stavrou <info@protesilaos.com>
|
||||
;; Maintainer: Denote Development <~protesilaos/denote@lists.sr.ht>
|
||||
;; URL: https://git.sr.ht/~protesilaos/denote
|
||||
;; Mailing-List: https://lists.sr.ht/~protesilaos/denote
|
||||
;; Version: 0.3.0
|
||||
;; Package-Requires: ((emacs "27.2"))
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
;;
|
||||
;; Used internally by commands that operate on file contents.
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'denote)
|
||||
(require 'xref)
|
||||
|
||||
(defconst denote-retrieve--title-front-matter-key-regexp
|
||||
"^\\(?:#\\+\\)?\\(?:title\\)\\s-*[:=]"
|
||||
"Regular expression for title key.")
|
||||
|
||||
(defconst denote-retrieve--id-front-matter-key-regexp
|
||||
"^.?.?\\b\\(?:identifier\\)\\s-*[:=]"
|
||||
"Regular expression for identifier key.")
|
||||
|
||||
(defconst denote-retrieve--date-front-matter-key-regexp
|
||||
"^\\(?:#\\+\\)?\\(?:date\\)\\s-*[:=]"
|
||||
"Regular expression for date key.")
|
||||
|
||||
(defun denote-retrieve--filename-identifier (file)
|
||||
"Extract identifier from FILE name."
|
||||
(if (file-exists-p file)
|
||||
(progn
|
||||
(string-match denote--id-regexp file)
|
||||
(match-string 0 file))
|
||||
(error "Cannot find `%s' as a file" file)))
|
||||
|
||||
(defun denote-retrieve--search (file key-regexp &optional key)
|
||||
"Return value of KEY-REGEXP key in current buffer from FILE.
|
||||
If optional KEY is non-nil, return the key instead."
|
||||
(with-temp-buffer
|
||||
(insert-file-contents file)
|
||||
(save-excursion
|
||||
(save-restriction
|
||||
(widen)
|
||||
(goto-char (point-min))
|
||||
(when (re-search-forward key-regexp nil t 1)
|
||||
(if key
|
||||
(match-string-no-properties 0)
|
||||
(let ((trims "[ \t\n\r\"']+"))
|
||||
(string-trim
|
||||
(buffer-substring-no-properties (point) (point-at-eol))
|
||||
trims trims))))))))
|
||||
|
||||
(defun denote-retrieve--value-title (file &optional key)
|
||||
"Return title value from FILE.
|
||||
If optional KEY is non-nil, return the key instead."
|
||||
(denote-retrieve--search file denote-retrieve--title-front-matter-key-regexp key))
|
||||
|
||||
(defun denote-retrieve--value-date (file &optional key)
|
||||
"Return date value from FILE.
|
||||
If optional KEY is non-nil, return the key instead."
|
||||
(denote-retrieve--search file denote-retrieve--date-front-matter-key-regexp key))
|
||||
|
||||
(defun denote-retrieve--read-file-prompt ()
|
||||
"Prompt for regular file in variable `denote-directory'."
|
||||
(read-file-name "Select note: " (denote-directory) nil nil nil
|
||||
(lambda (f) (or (denote--only-note-p f) (file-directory-p f)))))
|
||||
|
||||
(defun denote-retrieve--files-in-output (files)
|
||||
"Return list of FILES from `find' output."
|
||||
(delq nil (mapcar (lambda (f)
|
||||
(when (denote--only-note-p f) f))
|
||||
files)))
|
||||
|
||||
(defun denote-retrieve--xrefs (identifier)
|
||||
"Return xrefs of IDENTIFIER in variable `denote-directory'.
|
||||
The xrefs are returned as an alist."
|
||||
(xref--alistify
|
||||
(xref-matches-in-files identifier (denote--directory-files :absolute))
|
||||
(lambda (x)
|
||||
(xref-location-group (xref-item-location x)))))
|
||||
|
||||
(defun denote-retrieve--files-in-xrefs (xrefs)
|
||||
"Return sorted file names sans directory from XREFS.
|
||||
Parse `denote-retrieve--xrefs'."
|
||||
(sort
|
||||
(delete-dups
|
||||
(mapcar (lambda (x)
|
||||
(denote--file-name-relative-to-denote-directory (car x)))
|
||||
xrefs))
|
||||
#'string-lessp))
|
||||
|
||||
(defun denote-retrieve--proces-grep (identifier)
|
||||
"Process lines matching IDENTIFIER and return list of files."
|
||||
(let* ((default-directory (denote-directory))
|
||||
(file (denote--file-name-relative-to-denote-directory (buffer-file-name))))
|
||||
(denote-retrieve--files-in-output
|
||||
(delete file (denote-retrieve--files-in-xrefs
|
||||
(denote-retrieve--xrefs identifier))))))
|
||||
|
||||
(provide 'denote-retrieve)
|
||||
;;; denote-retrieve.el ends here
|
||||
|
|
@ -1,483 +0,0 @@
|
|||
;;; denote-test.el --- Unit tests for Denote -*- lexical-binding: t -*-
|
||||
|
||||
;; Copyright (C) 2023-2026 Free Software Foundation, Inc.
|
||||
|
||||
;; Author: Protesilaos <info@protesilaos.com>
|
||||
;; Maintainer: Protesilaos <info@protesilaos.com>
|
||||
;; URL: https://github.com/protesilaos/denote
|
||||
|
||||
;; This file is NOT part of GNU Emacs.
|
||||
|
||||
;; This program is free software; you can redistribute it and/or modify
|
||||
;; it under the terms of the GNU General Public License as published by
|
||||
;; the Free Software Foundation, either version 3 of the License, or
|
||||
;; (at your option) any later version.
|
||||
;;
|
||||
;; This program is distributed in the hope that it will be useful,
|
||||
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
;; GNU General Public License for more details.
|
||||
;;
|
||||
;; You should have received a copy of the GNU General Public License
|
||||
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
;;; Commentary:
|
||||
|
||||
;; Tests for Denote. Note that we are using Shorthands in this file,
|
||||
;; so the "dt-" prefix really is "denote-test-". Evaluate the
|
||||
;; following to learn more:
|
||||
;;
|
||||
;; (info "(elisp) Shorthands")
|
||||
|
||||
;;; Code:
|
||||
|
||||
(require 'ert)
|
||||
|
||||
;;;; Tests for denote.el
|
||||
|
||||
(require 'denote)
|
||||
|
||||
(ert-deftest dt-denote-directories--get-paths ()
|
||||
"Test that `denote-directories--get-paths' returns a list of paths."
|
||||
(let ((path "~/Documents/notes"))
|
||||
(should (equal
|
||||
(denote-directories--get-paths path)
|
||||
(list (file-name-as-directory (expand-file-name path))))))
|
||||
(let ((paths (list "~/Documents/notes" "~/Documents/books")))
|
||||
(should (equal
|
||||
(denote-directories--get-paths paths)
|
||||
(mapcar
|
||||
(lambda (path)
|
||||
(file-name-as-directory (expand-file-name path)))
|
||||
paths)))
|
||||
(should (seq-every-p
|
||||
(lambda (path)
|
||||
(string-suffix-p "/" path))
|
||||
(denote-directories--get-paths paths)))
|
||||
(should-not (null (denote-directories--get-paths paths))))
|
||||
(should-error (denote-directories--get-paths 'hello))
|
||||
(should-error (denote-directories--get-paths '(hello)))
|
||||
(should-error (denote-directories--get-paths '(1 2 3))))
|
||||
|
||||
(ert-deftest dt-denote--make-denote-directory ()
|
||||
"Test that `denote--make-denote-directory' creates the directory."
|
||||
(let* ((names (list "denote-test-made-directory-1" "denote-test-made-directory-2"))
|
||||
(directories (mapcar
|
||||
(lambda (directory)
|
||||
(file-name-as-directory (expand-file-name directory temporary-file-directory)))
|
||||
names)))
|
||||
(should-not (denote-directories--make-paths directories))
|
||||
(should (seq-every-p #'file-name-directory directories))
|
||||
(dolist (directory directories)
|
||||
(delete-directory directory))))
|
||||
|
||||
(ert-deftest dt-denote-directories ()
|
||||
"Test that `denote-directories' returns list of directories.
|
||||
It does so by expanding the value of the user option `denote-directory'.
|
||||
|
||||
Also see `denote-test--denote--make-denote-directory',
|
||||
`denote-test---denote-directories--get-paths'."
|
||||
(let ((denote-directory "/tmp/denote-test-notes"))
|
||||
(should
|
||||
(equal
|
||||
(denote-directories)
|
||||
(list (file-name-as-directory (expand-file-name denote-directory)))))
|
||||
(should (and (file-directory-p denote-directory)
|
||||
(file-name-absolute-p denote-directory))))
|
||||
(let ((denote-directory (list "/tmp/denote-test-notes-1" "/tmp/denote-test-notes-2")))
|
||||
(should
|
||||
(equal
|
||||
(denote-directories)
|
||||
(mapcar
|
||||
(lambda (directory)
|
||||
(file-name-as-directory (expand-file-name directory)))
|
||||
denote-directory)))
|
||||
(should (and (seq-every-p #'file-directory-p denote-directory)
|
||||
(seq-every-p #'file-name-absolute-p denote-directory)))))
|
||||
|
||||
(ert-deftest dt-denote-sluggify-title ()
|
||||
"Test that `denote-sluggify-title' removes punctuation from the string.
|
||||
Concretely, remove anything specified in `denote-sluggify-title'."
|
||||
(should (equal (denote-sluggify-title "this-is-!@#test") "this-is-test")))
|
||||
|
||||
(ert-deftest dt-denote-slug-keep-only-ascii ()
|
||||
"Test that `denote-slug-keep-only-ascii' removes non-ASCII characters."
|
||||
(should (equal
|
||||
(denote-slug-keep-only-ascii "There are no-ASCII : characters | here 😀")
|
||||
"There are no-ASCII characters here ")))
|
||||
|
||||
(ert-deftest dt-denote-slug-hyphenate ()
|
||||
"Test that `denote-slug-hyphenate' hyphenates the string.
|
||||
Also replace multiple hyphens with a single one and remove any
|
||||
leading and trailing hyphen."
|
||||
(should (equal (denote-slug-hyphenate "__ This is a test __ ") "This-is-a-test")))
|
||||
|
||||
(ert-deftest dt-denote-sluggify ()
|
||||
"Test that `denote-sluggify' sluggifies the string.
|
||||
To sluggify is to (i) downcase, (ii) hyphenate, (iii) de-punctuate, and (iv) remove spaces from the string."
|
||||
(should (equal (denote-sluggify 'title " ___ !~!!$%^ This iS a tEsT ++ ?? ") "this-is-a-test")))
|
||||
|
||||
(ert-deftest Ddenote--slug-put-equals ()
|
||||
"Test that `denote-slug-put-equals' replaces spaces/underscores with =.
|
||||
Otherwise do the same as what is described in
|
||||
`denote-test--denote-slug-hyphenate'.
|
||||
|
||||
The use of the equals sign is for the SIGNATURE field of the
|
||||
Denote file name."
|
||||
(should (equal (denote-slug-put-equals "__ This is a test __ ") "This=is=a=test")))
|
||||
|
||||
(ert-deftest dt-denote-sluggify-signature ()
|
||||
"Test that `denote-sluggify-signature' sluggifies the string for file signatures.
|
||||
This is like `denote-test--denote-sluggify', except that it also
|
||||
accounts for what we describe in `denote-test--denote-slug-put-equals'."
|
||||
(should (equal (denote-sluggify-signature "--- ___ !~!!$%^ This -iS- a tEsT ++ ?? ") "this=is=a=test")))
|
||||
|
||||
(ert-deftest dt-denote-sluggify-keyword ()
|
||||
"Test that `denote-sluggify-keyword' sluggifies the string while joining words.
|
||||
In this context, to join words is to elimitate any space or
|
||||
delimiter between them.
|
||||
|
||||
Otherwise, this is like `denote-test--denote-sluggify'."
|
||||
(should (equal (denote-sluggify-keyword "--- ___ !~!!$%^ This iS a - tEsT ++ ?? ") "thisisatest")))
|
||||
|
||||
(ert-deftest dt-denote-sluggify-keywords ()
|
||||
"Test that `denote-sluggify-keywords' sluggifies a list of strings.
|
||||
The function also account for the value of the user option
|
||||
`denote-allow-multi-word-keywords'."
|
||||
(should (equal (denote-sluggify-keywords '("one !@# --- one" " two" "__ three __")) '("oneone" "two" "three"))))
|
||||
|
||||
(ert-deftest dt-denote-slug-remove-accents ()
|
||||
"Test that `denote-slug-remove-accents' removed diacretics."
|
||||
(should (string= (denote-slug-remove-accents "ê") "e"))
|
||||
(should (string= (denote-slug-remove-accents "ñ") "n")))
|
||||
|
||||
(ert-deftest dt-denote--file-empty-p ()
|
||||
"Test that `denote--file-empty-p' returns non-nil on empty file."
|
||||
(let ((file (make-temp-file "denote-test")))
|
||||
(should (denote--file-empty-p file))
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(insert "Hello world, this is not empty anymore!")
|
||||
(save-buffer))
|
||||
(should-not (denote--file-empty-p file))
|
||||
(delete-file file)))
|
||||
|
||||
(ert-deftest dt-denote-file-has-denoted-filename-p ()
|
||||
"Test that `denote-file-has-denoted-filename-p' validates a Denote file name."
|
||||
(should (denote-file-has-denoted-filename-p "20230522T154900--test__keyword.txt"))
|
||||
(should-not (denote-file-has-denoted-filename-p "hello")))
|
||||
|
||||
(ert-deftest dt-denote-file-has-identifier-p ()
|
||||
"Test that `denote-file-has-identifier-p' checks for a Denote identifier."
|
||||
(should (denote-file-has-identifier-p "20230522T154900--test__keyword.txt"))
|
||||
(should-not (denote-file-has-identifier-p "T154900--test__keyword.txt")))
|
||||
|
||||
(ert-deftest dt-denote-file-has-signature-p ()
|
||||
"Test that `denote-file-has-signature-p' checks for a Denote signature."
|
||||
(should (denote-file-has-signature-p "20230522T154900==sig--test__keyword.txt"))
|
||||
(should-not (denote-file-has-signature-p "20230522T154900--test__keyword.txt")))
|
||||
|
||||
(ert-deftest dt-denote-file-has-supported-extension-p ()
|
||||
"Test that `denote-file-has-supported-extension-p' matches a supported extension."
|
||||
(let ((extensions (denote-file-type-extensions-with-encryption)))
|
||||
(should
|
||||
(member
|
||||
(file-name-extension "20230522T154900==sig--test__keyword.txt" :period)
|
||||
extensions))
|
||||
(should-not
|
||||
(member
|
||||
(file-name-extension "20230522T154900==sig--test__keyword" :period)
|
||||
extensions))))
|
||||
|
||||
(ert-deftest dt-denote-file-type-extensions ()
|
||||
"Test that `denote-file-type-extensions' returns file extensions.
|
||||
We check for the common file type extensions, though the user can set
|
||||
`denote-file-types' to nil and handle things on their own or, anyhow,
|
||||
modify that variable."
|
||||
(let ((extensions (denote-file-type-extensions)))
|
||||
(should (or (member ".md" extensions)
|
||||
(member ".org" extensions)
|
||||
(member ".txt" extensions)))))
|
||||
|
||||
(ert-deftest dt-denote-file-type-extensions-with-encryption ()
|
||||
"Test that `denote-file-type-extensions-with-encryption' covers encryption.
|
||||
Extend what we do in `denote-test--denote-file-type-extensions'."
|
||||
(let ((extensions (denote-file-type-extensions-with-encryption)))
|
||||
(should (or (member ".md" extensions)
|
||||
(member ".org" extensions)
|
||||
(member ".txt" extensions)
|
||||
(member ".md.gpg" extensions)
|
||||
(member ".org.gpg" extensions)
|
||||
(member ".txt.gpg" extensions)
|
||||
(member ".md.age" extensions)
|
||||
(member ".org.age" extensions)
|
||||
(member ".txt.age" extensions)))))
|
||||
|
||||
(ert-deftest dt-denote--format-front-matter ()
|
||||
"Test that `denote--format-front-matter' formats front matter correctly.
|
||||
To make the test reproducible, set `denote-date-format' to a value that
|
||||
does not involve the time zone."
|
||||
(let ((denote-date-format "%Y-%m-%d")
|
||||
(denote-front-matter-components-present-even-if-empty-value '(title keywords signature date identifier)))
|
||||
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") '("") "" "" 'text)
|
||||
(string-join
|
||||
'("title: "
|
||||
"date: 2024-01-01"
|
||||
"tags: "
|
||||
"identifier: "
|
||||
"signature: "
|
||||
"---------------------------\n\n")
|
||||
"\n")))
|
||||
(should (equal (denote--format-front-matter "Some test" (date-to-time "2023-06-05") '("one" "two") "20230605T102234" "sig" 'text)
|
||||
(string-join
|
||||
'("title: Some test"
|
||||
"date: 2023-06-05"
|
||||
"tags: one two"
|
||||
"identifier: 20230605T102234"
|
||||
"signature: sig"
|
||||
"---------------------------\n\n")
|
||||
"\n")))
|
||||
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") nil "" "" 'org)
|
||||
(string-join
|
||||
'("#+title: "
|
||||
"#+date: 2024-01-01"
|
||||
"#+filetags: "
|
||||
"#+identifier: "
|
||||
"#+signature: "
|
||||
"\n")
|
||||
"\n")))
|
||||
(should (equal
|
||||
(denote--format-front-matter
|
||||
"Some test" (date-to-time "2023-06-05") '("one" "two")
|
||||
"20230605T102234" "sig" 'org)
|
||||
(string-join
|
||||
'("#+title: Some test"
|
||||
"#+date: 2023-06-05"
|
||||
"#+filetags: :one:two:"
|
||||
"#+identifier: 20230605T102234"
|
||||
"#+signature: sig"
|
||||
"\n")
|
||||
"\n")))
|
||||
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") nil "" "" 'markdown-yaml)
|
||||
(string-join
|
||||
'("---"
|
||||
"title: \"\""
|
||||
"date: 2024-01-01"
|
||||
"tags: []"
|
||||
"identifier: \"\""
|
||||
"signature: \"\""
|
||||
"---"
|
||||
"\n")
|
||||
"\n")))
|
||||
(should (equal (denote--format-front-matter "Some test" (date-to-time "2023-06-05") '("one" "two") "20230605T102234" "sig" 'markdown-yaml)
|
||||
(string-join
|
||||
'("---"
|
||||
"title: \"Some test\""
|
||||
"date: 2023-06-05"
|
||||
"tags: [\"one\", \"two\"]"
|
||||
"identifier: \"20230605T102234\""
|
||||
"signature: \"sig\""
|
||||
"---"
|
||||
"\n")
|
||||
"\n")))
|
||||
(should (equal (denote--format-front-matter "" (date-to-time "20240101T120000") nil "" "" 'markdown-toml)
|
||||
(string-join
|
||||
'("+++"
|
||||
"title = \"\""
|
||||
"date = 2024-01-01"
|
||||
"tags = []"
|
||||
"identifier = \"\""
|
||||
"signature = \"\""
|
||||
"+++"
|
||||
"\n")
|
||||
"\n")))
|
||||
(should (equal (denote--format-front-matter "Some test" (date-to-time "2023-06-05") '("one" "two") "20230605T102234" "sig" 'markdown-toml)
|
||||
(string-join
|
||||
'("+++"
|
||||
"title = \"Some test\""
|
||||
"date = 2023-06-05"
|
||||
"tags = [\"one\", \"two\"]"
|
||||
"identifier = \"20230605T102234\""
|
||||
"signature = \"sig\""
|
||||
"+++"
|
||||
"\n")
|
||||
"\n")))))
|
||||
|
||||
(ert-deftest dt-denote-format-file-name ()
|
||||
"Test that `denote-format-file-name' returns all expected paths."
|
||||
(let* ((title "Some test")
|
||||
(id (format-time-string denote-date-identifier-format (denote-valid-date-p "2023-11-28 05:53:11")))
|
||||
(denote-directory "/tmp/test-denote")
|
||||
(dir (car (denote-directories)))
|
||||
(ext (denote--file-extension 'org))
|
||||
(kws '("one" "two")))
|
||||
(should-error (denote-format-file-name nil id kws title ext ""))
|
||||
(should-error (denote-format-file-name "" id kws title ext ""))
|
||||
;; NOTE that `denote-directory' is the `let' bound value without the suffix
|
||||
(should-error (denote-format-file-name denote-directory id kws title ext ""))
|
||||
(should-error (denote-format-file-name dir "" nil "" ext ""))
|
||||
(should (equal (denote-format-file-name dir nil kws title ext "") "/tmp/test-denote/--some-test__one_two.org"))
|
||||
(should (equal (denote-format-file-name dir "" kws title ext "") "/tmp/test-denote/--some-test__one_two.org"))
|
||||
(should (equal (denote-format-file-name dir "0123456" kws title ext "") "/tmp/test-denote/@@0123456--some-test__one_two.org"))
|
||||
(should (equal (denote-format-file-name dir id kws title ext "") "/tmp/test-denote/20231128T055311--some-test__one_two.org"))
|
||||
(should (equal (denote-format-file-name dir id nil "" ext "") "/tmp/test-denote/20231128T055311.org"))
|
||||
(should (equal (denote-format-file-name dir id nil nil ext nil) "/tmp/test-denote/20231128T055311.org"))
|
||||
(should (equal (denote-format-file-name dir id kws title ext "sig") "/tmp/test-denote/20231128T055311==sig--some-test__one_two.org"))))
|
||||
|
||||
(ert-deftest dt-denote-get-file-extension ()
|
||||
"Test that `denote-get-file-extension' gets the correct file extension."
|
||||
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing") ""))
|
||||
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org") ".org"))
|
||||
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org.gpg") ".org.gpg"))
|
||||
(should (equal (denote-get-file-extension "20231010T105034--some-test-file__denote_testing.org.age") ".org.age")))
|
||||
|
||||
(ert-deftest dt-denote-get-file-extension-sans-encryption ()
|
||||
"Test that `denote-get-file-extension-sans-encryption' gets the file extension without encryption."
|
||||
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing") ""))
|
||||
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org") ".org"))
|
||||
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org.gpg") ".org"))
|
||||
(should (equal (denote-get-file-extension-sans-encryption "20231010T105034--some-test-file__denote_testing.org.age") ".org")))
|
||||
|
||||
;; TODO 2026-04-06: For the `denote-file-type'.
|
||||
|
||||
(ert-deftest dt-denote-filetype-heuristics ()
|
||||
"Test that `denote-filetype-heuristics' gets the correct file type."
|
||||
(should-not (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing"))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org") 'org))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org.gpg") 'org))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.org.age") 'org))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt") 'text))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt.gpg") 'text))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.txt.age") 'text))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md")
|
||||
(caar (denote--file-types-with-extension ".md"))))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md.gpg")
|
||||
(caar (denote--file-types-with-extension ".md"))))
|
||||
(should (eq (denote-filetype-heuristics "20231010T105034--some-test-file__denote_testing.md.age")
|
||||
(caar (denote--file-types-with-extension ".md")))))
|
||||
|
||||
(ert-deftest dt-denote-retrieve-filename-identifier ()
|
||||
"Test that `denote-retrieve-filename-identifier' returns only the identifier."
|
||||
(should (null (denote-retrieve-filename-identifier "/path/to/testing/--this-is-a-test-reordered__denote_testing.org")))
|
||||
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org") "20240610T194654"))
|
||||
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "20240610T194654"))
|
||||
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org") "20240610T194654"))
|
||||
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org") "20240610T194654"))
|
||||
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "20240610T194654"))
|
||||
(should (equal (denote-retrieve-filename-identifier "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "20240610T194654")))
|
||||
|
||||
(ert-deftest dt-denote-retrieve-filename-title ()
|
||||
"Test that `denote-retrieve-filename-title' returns only the title."
|
||||
(should (null (denote-retrieve-filename-title "/path/to/testing/20240610T194654__denote_testing.org")))
|
||||
(should (equal (denote-retrieve-filename-title "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org") "this-is-a-test-reordered"))
|
||||
(should (equal (denote-retrieve-filename-title "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "this-is-a-test-reordered"))
|
||||
(should (equal (denote-retrieve-filename-title "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org") "this-is-a-test-reordered"))
|
||||
(should (equal (denote-retrieve-filename-title "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org") "this-is-a-test-reordered"))
|
||||
(should (equal (denote-retrieve-filename-title "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "this-is-a-test-reordered"))
|
||||
(should (equal (denote-retrieve-filename-title "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "this-is-a-test-reordered")))
|
||||
|
||||
(ert-deftest dt-denote-retrieve-filename-keywords ()
|
||||
"Test that `denote-retrieve-filename-keywords' returns only the keywords."
|
||||
(should (null (denote-retrieve-filename-keywords "/path/to/testing/20240610T194654--this-is-a-test-reordered.org")))
|
||||
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org") "denote_testing"))
|
||||
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "denote_testing"))
|
||||
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/--this-is-a-test-reordered__denote_testing@@20240610T194654.org") "denote_testing"))
|
||||
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/__denote_testing--this-is-a-test-reordered@@20240610T194654.org") "denote_testing"))
|
||||
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "denote_testing"))
|
||||
(should (equal (denote-retrieve-filename-keywords "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "denote_testing")))
|
||||
|
||||
(ert-deftest dt-denote-retrieve-filename-signature ()
|
||||
"Test that `denote-retrieve-filename-signature' returns only the signature."
|
||||
(should (null (denote-retrieve-filename-signature "/path/to/testing/20240610T194654--this-is-a-test-reordered__denote_testing.org")))
|
||||
(should (equal (denote-retrieve-filename-signature "/path/to/testing/20240610T194654==signature--this-is-a-test-reordered__denote_testing.org") "signature"))
|
||||
(should (equal (denote-retrieve-filename-signature "/path/to/testing/--this-is-a-test-reordered==signature__denote_testing@@20240610T194654.org") "signature"))
|
||||
(should (equal (denote-retrieve-filename-signature "/path/to/testing/__denote_testing--this-is-a-test-reordered==signature@@20240610T194654.org") "signature"))
|
||||
(should (equal (denote-retrieve-filename-signature "/path/to/testing/__denote_testing@@20240610T194654--this-is-a-test-reordered==signature.org") "signature"))
|
||||
(should (equal (denote-retrieve-filename-signature "/path/to/testing/==signature__denote_testing@@20240610T194654--this-is-a-test-reordered.org") "signature")))
|
||||
|
||||
(ert-deftest dt-denote-date-identifier-p ()
|
||||
"Test that `denote-date-identifier-p' works for Denote identifiers."
|
||||
(should (denote-date-identifier-p "20240901T090910"))
|
||||
(should-not (denote-date-identifier-p "20240901T090910-not-identifier-format")))
|
||||
|
||||
(ert-deftest dt-denote-id-to-date ()
|
||||
"Test that `denote-id-to-date' returns the date from an identifier."
|
||||
(should (equal (denote-id-to-date "20240901T090910") "2024-09-01"))
|
||||
(should-error (denote-id-to-date "20240901T090910-not-identifier-format")))
|
||||
|
||||
(ert-deftest dt-denote--date-convert ()
|
||||
"Test that `denote--date-convert' works with dates."
|
||||
(should (equal (denote--date-convert '(26454 45206 461174 657000) :list) '(26454 45206 461174 657000)))
|
||||
(should (equal (denote--date-convert '(26454 45206 461174 657000) :string) "2024-12-09 10:55:50"))
|
||||
(should (equal (denote--date-convert nil :string) ""))
|
||||
(should (equal (denote--date-convert nil :list) nil))
|
||||
(should-error (denote--date-convert '(26454 45206 461174 657000) :not-valid-type))
|
||||
(should-error (denote--date-convert nil :not-valid-type)))
|
||||
|
||||
;; TODO 2026-02-23: Add support for Windows paths. But I need somebody who has a Windows machine...
|
||||
(ert-deftest dt-denote--get-common-root-directory ()
|
||||
"Test that `denote--get-common-root-directory' returns the right path."
|
||||
(should (string=
|
||||
(denote--get-common-root-directory
|
||||
'("/home/prot/Documents/notes/"
|
||||
"/home/prot/Documents/notes/attachments"))
|
||||
"/home/prot/Documents/notes/"))
|
||||
(should (string=
|
||||
(denote--get-common-root-directory
|
||||
'("/home/prot/Documents/"
|
||||
"/home/prot/Documents/notes/attachments"))
|
||||
"/home/prot/Documents/"))
|
||||
(should (string=
|
||||
(denote--get-common-root-directory
|
||||
'("/home/prot/Books/"
|
||||
"/home/prot/Documents/notes/"))
|
||||
"/home/prot/"))
|
||||
(should (string=
|
||||
(denote--get-common-root-directory
|
||||
'("/tmp/notes/"
|
||||
"/home/prot/Documents/notes/"))
|
||||
"/"))
|
||||
(should (string=
|
||||
(denote--get-common-root-directory
|
||||
'("/home/prot/Documents/notes/"))
|
||||
"/home/prot/Documents/notes/"))
|
||||
(should-error (denote--get-common-root-directory '("~/Documents/notes/"))))
|
||||
|
||||
(ert-deftest dt-denote-query--keywords-as-regexp ()
|
||||
"Test that `denote-query--keywords-as-regexp' works as intended."
|
||||
(should (string= (denote-query--keywords-as-regexp '("one" "two")) "_\\(?:one\\|two\\)"))
|
||||
(should-error (denote-query--keywords-as-regexp "one"))
|
||||
(should-error (denote-query--keywords-as-regexp '("one" 1))))
|
||||
|
||||
(ert-deftest dt-denote-file-type ()
|
||||
"Test that function `denote-file-type' does what it is meant to."
|
||||
(let* ((create-file-fn (lambda (name extension)
|
||||
(expand-file-name
|
||||
(format "denote-test-file-type-%s.%s" name extension)
|
||||
(temporary-file-directory))))
|
||||
(modify-file-fn (lambda (file contents)
|
||||
(with-current-buffer (find-file-noselect file)
|
||||
(erase-buffer)
|
||||
(insert contents)
|
||||
(save-buffer))))
|
||||
(yaml-file (funcall create-file-fn "yaml" "md"))
|
||||
(toml-file (funcall create-file-fn "toml" "md"))
|
||||
(org-file (funcall create-file-fn "org" "org")))
|
||||
(progn
|
||||
(funcall modify-file-fn yaml-file denote-yaml-front-matter)
|
||||
(should (eq (denote-file-type yaml-file) 'markdown-yaml)))
|
||||
(progn
|
||||
(funcall modify-file-fn toml-file denote-toml-front-matter)
|
||||
(should (eq (denote-file-type toml-file) 'markdown-toml)))
|
||||
(progn
|
||||
(funcall modify-file-fn org-file denote-org-front-matter)
|
||||
(should (eq (denote-file-type org-file) 'org)))
|
||||
(should-not (denote-file-type "test"))
|
||||
(let ((denote-file-types nil))
|
||||
(should-not (denote-file-type "test.md")))))
|
||||
|
||||
(provide 'denote-test)
|
||||
;;; denote-test.el ends here
|
||||
|
||||
;; Local Variables:
|
||||
;; read-symbol-shorthands: (("dt" . "denote-test-"))
|
||||
;; End:
|
||||
Loading…
Reference in a new issue