Denote Emacs Configuration

A Literate Configuration

update: The updated version of my Denote config is available at https://git.sr.ht/~jeremyf/dotemacs/tree/main/item/emacs.d/jf-denote.el

update: My “live” version of my Denote config is available at https://github.com/jeremyf/dotemacs/blob/main/emacs.d/denote-emacs-configuration.org

I wrote about Exploring the Denote Emacs Package and Migration Plan for Org-Roam Notes to Denote; this is now my configuration for using Denote. This sits orthogonal to my data migration of notes into Denote paradigm.

I’m writing the data migration scripts in Ruby 📖; I’m more fluent in that language and found it just a easier to conceptualize the migration. As of , I have completed a test migration of my glossary.

Configuration

This base configuration gets me started using Denote.

(use-package denote
  ;; I want to point to cutting edge development; there's already features I
  ;; want that have been added since v1.0.0
  :straight (denote :host nil :type git :repo "https://git.sr.ht/~protesilaos/denote")
  :commands (denote-directory denote-file-prompt denote--title-prompt)
  :bind ("H-f" . 'jf/denote-find-file)
  :hook (dired-mode . denote-dired-mode)
  :custom ((denote-directory (expand-file-name "denote" org-directory)
			     ;; These are the minimum viable prompts for notes
			     (denote-prompts '(title keywords))
			     ;; I love ‘org-mode format; reading ahead I'm setting this
			     (denote-file-type 'org)
			     ;; And `org-read-date' is an amazing bit of tech
			     (denote-date-prompt-denote-date-prompt-use-org-read-date t)))
  :config
  (cl-defun jf/denote-org-property-from-id (&key identifier property)
    "Given an IDENTIFIER and PROPERTY return it's value or nil.

  Return nil when:

  - is not a denote file
  - IDENTIFIER leads to a non `org-mode' file
  - PROPERTY does not exist on the file"
    (when-let ((filename (denote-get-path-by-id identifier)))
      (when (string= (file-name-extension filename) "org")
	(with-current-buffer (find-file-noselect filename)
	  (cadar (org-collect-keywords (list property)))))))

  (cl-defun jf/denote-org-properties-from-id (&key identifier properties)
    "Given an IDENTIFIER and PROPERTIES list return an a-list of values.

  Return nil when:

  - is not a denote file
  - IDENTIFIER leads to a non `org-mode' file
  - PROPERTY does not exist on the file"
    (when-let ((filename (denote-get-path-by-id identifier)))
      (when (string= (file-name-extension filename) "org")
	(with-current-buffer (find-file-noselect filename)
	  (org-collect-keywords properties)))))
  )
  ;;; Testing jf/denote-org-property-from-id
;; (message "%s" (jf/denote-org-property-from-id :identifier "20220930T215235"
;; 					      :property "ABBR"))
;;; Testing jf/denote-org-properties-from-id
;; (message "%s" (jf/denote-org-properties-from-id :identifier "20220930T215235"
;; 					      :properties '("TITLE" "ABBR")))

From this configuration, I’ll build out my interactions.

Foundational Functions for my Denote Interaction

In Denote’s documentation for Convenience commands for note creation, they locally bind the denote-prompts variable then call denote; in Maintain separate directories for notes they mention having silos of information by using .dir-locals.el to cordon off different domains. See the Domains section of this file for more infromation.

And while I might want to do that for my domains I have the use case of wanting to link to a Glossary item from other domains.

I envision the jf/denote-find-file as the anchor for my general file finding, and then using a local binding of denote-directory to provide an initial narrowing default.

It doesn’t take much to see a macro emerging for these local bindings; find all sub-directories of the directory defiend by the denote-directory variable. For each of those directories make an interactive function of the form jf/denote-find-file--domain. That interactive function would locally bind denote-directory to ”denote-directory/domain”.

Alternatively, as I create each domain’s create function, I also create the finder function.

(defun jf/denote-find-file ()
      "Find file in the current `denote-directory'."
      (interactive)
      (require 'consult-projectile)
      (require 'denote)
      (consult-projectile--file (denote-directory)))

(cl-defmacro jf/denote-create-functions-for (&key domain)
  "A macro to create functions for the given DOMAIN.

Creates:

- Wrapping function of `jf/denote-find-file' that narrows results
  to the given DOMAIN."
  (let ((defun-fn (intern (concat "jf/denote-find-file--" domain)))
	(docstring (concat "Find file in \""
			   domain
			   "\" subdirectory of `denote-directory'.")))
    `(defun ,defun-fn ()
       ,docstring
       (interactive)
       (let ((denote-directory (f-join (denote-directory) ,domain)))
	 (call-interactively #'jf/denote-find-file)))))

Domains

In Migration Plan for Org-Roam Notes to Denote I talked about data structures and starting articulating some domains.

From Webster’s 1913 Dictionary: Domain: The territory over which dominion or authority is exerted; the possessions of a sovereign or commonwealth, or the like. Also used figuratively.

And the Mathematical concept of Domain: The domain of a function is the set of its possible inputs, i.e., the set of input values where for which the function is defined.

As of <2022-10-02 Sun>, my denote finding implementation leverages consult-projectile--file. This populates the mini-buffer with entries of the following format: domain/identifier--multi-word-title_tag1_tag2.org. The domain is a subdirectory of my denote-directory.

I have the domains following:

Blog Post
Something I share with the world.
Dailies
An anchor for any time references.
Employer
More specifically, Scientist.com.
Epigraph
A quote that I found interesting.
Glossary
A term/concept I reference.
People
Similar to a glossary but for notes regarding people.

Blog Post

When I start writing a note, I am uncertain if it will be a Blog Post. However, once I publish something I think it makes sense to transfer the note into the Blog Post domain. By treating a Blog Post as a domain it will be visually chunked at the beginning of the line (e.g. the subdirectory).

Alternatively I could add the “blog-post” keyword/tag to the note. The primary benefit would be that something I post to my blog could be of another domain.

What might those other domains be?

I don’t think I need linger on this for too long, as I can easily migrate. The foundational element is the identifier; which is dynamically queried.

Dailies

While writing this document, I began envisioning replacing my Org-Mode 📖 date macro with a date Org-Mode link protocol. The benefits are:

  1. Replacing the kludge of a macro with something that works easier with exports.
  2. I would be creating a node that could provide a backlink.

None of this requires Denote but which builds on some of my musings; namely should I have a monthly timesheet in Denote. And the answer appears to be yes.

(jf/denote-create-functions-for :domain "dailies")

I want to continue using my timesheets as a single document; this makes both time reporting and personal timetracking easier.

Employer

There are certain employer specific notes that I keep; timesheets being a distinct one. I don’t envision a problem linking to other domains; a Scientist.com note could and would likely link to/reference a Glossary entry.

The primary advantage is that I can easily segement my git repositories for employer and not-employer.

I need a current timesheet function; this would help me jump to my time sheet and capture appropriate tasks, projects, merge requests and blockers.

I also want my org-agenda-files to include:

  • personal agenda
  • work agenda (on work machine)
  • this month and last month’s time sheet

I’m okay with restarting Emacs 📖 each month.

(jf/denote-create-functions-for :domain "scientist")

Epigraph

As mentioned, I collect phrases and like to reference them as epigraphs in my posts.

Something in the Epigraph domain has the following properties:

AUTHOR_NAME (required)
The name of the author
AUTHOR_URL
Where can you “find” this author?
AUTHOR_KEY
The GLOSSARY_KEY for the given author
WORK_TITLE (required)
What’s the title of the work?
WORK_URL
Where can you “get” this work?
WORK_KEY
The GLOSSARY_KEY for the given work
POEM
Indicates if this is a poem (or not)
PAGE
The page in which this passage appears in the given work.
TRANSLATOR_NAME
The name of the translator

As part of my blog build scripts, I lookup the KEY properties in the Glossary and write the names and URL. With all of the changes I’ve made, I need to see if I’m still looking up the KEY properties when I build the script.

(bind-key "H-d c e" 'jf/denote-create-epigraph)
(cl-defun jf/denote-create-epigraph (&key
				     (body (read-from-minibuffer "Epigraph Text: "))
				     ;; Todo prompt for Author Name
				     (author_name (read-from-minibuffer "Author Name: "))
				     ;; Todo prompt for Work Title
				     (work_title (read-from-minibuffer "Work Title: "))
				     (nth-words 8))
  "Create an epigraph from the given BODY, AUTHOR_NAME, and WORK TITLE.

Default the note’s title to the first NTH-WORDS of the BODY."
  (interactive)
  (let* ((body-as-list (s-split-words body))
	 (title (s-join " " (if (> (length body-as-list) nth-words)
				(subseq body-as-list 0 nth-words)
			      body-as-list)))
	 (template (concat
		    "#+AUTHOR_NAME: " author_name "\n"
		    "#+AUTHOR_URL:\n"
		    "#+AUTHOR_KEY:\n"
		    "#+WORK_TITLE: " work_title "\n"
		    "#+WORK_URL:\n"
		    "#+WORK_KEY:\n"
		    "#+POEM:\n"
		    "#+PAGE:\n"
		    "#+TRANSLATOR_NAME:\n")))
    (denote title
	    nil
	    'org
	    (f-join (denote-directory) "epigraphs")
	    nil
	    template)))

(jf/denote-create-functions-for :domain "epigraphs")

Glossary

We’ll store glossary entries in the “glossary” subdirectory of denote-directory.

An entry in the glossary requires a KEY property. This KEY is used as the entry point for my blogging glossary.html shortcode.

All other properties, aside from TITLE, are optional. In my writing there are two ways I directly refer to a glossary entry, when I:

  • Reference a Game
  • Use an Abbreviation

I might create two or three glossary entries at a time; so the easiest approach is to include all of the properties with minimal prompting.

(bind-key "H-d c g" 'jf/denote-create-glossary-entry)
(cl-defun jf/denote-create-glossary-entry
    (&key
     (title (read-from-minibuffer "Name the Entry: "))
     (is-a-game (yes-or-no-p "Is this a game?"))
     (abbr (read-from-minibuffer "Abbreviation (empty to skip): ")))
  "Create a `denote' entry for the given TITLE and ABBR.

And if this IS-A-GAME then amend accordingly.

      NOTE: At present there is no consideration for uniqueness."
  (interactive)
  (let* ((key (downcase (denote-sluggify (if (s-present? abbr) abbr title))))
	 (template (concat "#+GLOSSARY_KEY: " key "\n"
			   "#+ABBR:" (when (s-present? abbr) (concat " " abbr)) "\n"
			   "#+CONTENT_DISCLAIMER:\n" ;; TODO: Include a prompt of existing disclaimers
'			   "#+DESCRIPTION:\n"
			   (when is-a-game "#+GAME: " key "\n")
			   "#+ITEMID:\n"
			   "#+ITEMTYPE:\n"
			   "#+MENTION_AS:\n"
			   "#+OFFER:\n"
			   "#+PLURAL_ABBR:\n"
			   "#+PLURAL_TITLE:\n"
			   "#+SAME_AS:\n"
			   "#+TAG:\n" ;; TODO: Assert uniqueness
			   "#+VERBOSE_TITLE:\n"))
	 (keywords (list)))
    ;; Add both "abbr" and the abbr to the keywords; both help in searching results
    (when (s-present? abbr)
      (progn (add-to-list 'keywords "abbr") (add-to-list 'keywords abbr)))
    (when is-a-game (add-to-list 'keywords "game"))
    (denote title
	    keywords
	    'org
	    (f-join (denote-directory) "glossary")
	    nil
	    template)))

(jf/denote-create-functions-for :domain "glossary")
;;; Testing jf/denote-org-property-from-id
;; (message "%s" (jf/denote-org-property-from-id :id "20220930T215235"
;; 					      :property "ABBR"))

This builds from On Storing Glossary Terms in Org Roam Nodes.

People

I do write notes about people I interact with. Technically I have glossary entries for people. But those entries are for folks I don’t interact with.

(jf/denote-create-functions-for :domain "people")

I have two custom hyperlinks to consider:

  • Abbrevations (and their Plural)
  • Date entries

Abbreviations (and their Plural)

I wrote about On Storing Glossary Terms in Org Roam Nodes. This builds from and supplants that prior work.

As part of my writing I use of abbreviations. I try to always provide the abbreviation’s title when I first introduce the abbrevation. For most of those abbreviations I reference something in my glossary.

When I export to my blog, I want those abbreviations to leverage what I have in my local glossary. I expand those abbreviatinos to use the ABBR-element. I do this via my glossary.html shortcode.

Below is the code that adds the abbr and abbr-plural link type into Org-Mode’s link handler; for more information checkout the documentation on Adding Hyperlink Types.

Building the Complete Functionality

First up is the functionality for completion. Given that I have both abbr and abbr-plural link schemes, I’m going to create a generic function.

I prefer named parameters over positional parameters. Which is why most of my Lisp functions leverage the cl-defun macro.

The jf/org-link-complete-link-for function will pre-populate a search. In the case of abbr and abbr-plural all entries will be in the ./glossary subdirectory and have the keyword _abbr.

(cl-defun jf/org-link-complete-link-for (parg &key scheme keyword subdirectory))
      "Prompt for a SCHEME compatible `denote' with KEYWORD in the given SUBDIRECTORY.

      Returns a string of format: \"SCHEME:<id>\" where <id> is
      an `denote' identifier."
      (concat scheme
	      ":"
	      (let ((denote-directory (if subdirectory
					  (f-join (denote-directory)
						  (concat subdirectory "/"))
					(denote-directory))))
		;; This leverages a post v1.0.0 parameter of Denote
		;; See https://git.sr.ht/~protesilaos/denote/commit/c6c3fc95c66ba093a266c775f411c0c8615c14c7
		(denote-file-prompt (concat "_" keyword "*")))))

The above implementation assumes a post v1.0.0 implementation of Denote. As of this is not part of a released version but is part of the main branch.

I was preparing to send a suggestion for that feature when I noticed the change; it is always reassuring to see folks recommend functions that are identical to what you were going to suggest.

Building the Export Functionality

Next is the export functionality. There are many similarities between abbr and abbr-plural; what follows is the general function.

(cl-defun jf/denote-link-ol-link-with-property (link description format protocol
						     &key
						     property-name
						     additional-hugo-parameters
						     (use_hugo_shortcode jf/exporting-org-to-tor))
  "Export a LINK with DESCRIPTION for the given PROTOCOL and FORMAT.

      FORMAT is an Org export backend. We will discard the given
      DESCRIPTION.  PROTOCOL is ignored."
  (let* ((prop-list (jf/denote-org-properties-from-id
		     :identifier link
		     :properties (list "TITLE" property-name  "GLOSSARY_KEY")))
	 (title (alist-get "TITLE" prop-list nil nil #'string=))
	' (property (alist-get property-name prop-list nil nil #'string=))
	 (key (alist-get "GLOSSARY_KEY" prop-list property nil #'string=))
	 (cond
	  ((or (eq format 'html)
	       (eq format 'md))
	   (if use_hugo_shortcode
	       (format "\{\{< glossary key=\"%s\" %s >\}\}"
		       property
		       additional-hugo-parameters)
	     (format "<abbr title=\"%s\">%s</abbr>"
		     title
		     property))
	   (_ (format "%s (%s)"
		      title
		      property)))))))

With the above preliminaries, here are the two parameter types and their configurations.

(org-link-set-parameters "abbr"
			 :complete (lambda (&optional parg) (jf/org-link-complete-link-for
							     parg
							     :scheme "abbr"
							     :keyword "abbr"
							     :subdirectory "glossary"))
			 :export (lambda (link description format protocol)
				   (jf/denote-link-ol-link-with-property link description format protocol
									 :property-name "ABBR"
									 :additional-hugo-parameters "abbr=\"t\""))
			 :face #'denote-faces-link
			 :follow #'denote-link-ol-follow
			     ;;; I'm unclear if/how I want to proceed with this
			 ;; :store (lambda (jf/org-link-store-link-for :scheme "abbr"))
			 )

    (org-link-set-parameters "abbr-plural"
			     :complete (lambda (&optional parg) (jf/org-link-complete-link-for
								 parg
								 :scheme "abbr-plural"
								 :keyword "abbr_plural"
								 :subdirectory "glossary"))
			     :export (lambda (link description format protocol)
				       (jf/denote-link-ol-link-with-property link description format protocol
					  :property-name "ABBR_PLURAL"
					  :additional-hugo-parameters "abbr=\"t\" plural=\"t\"")
			     :face #'denote-faces-link
			     :follow #'denote-link-ol-follow
			     ;;; I'm unclear if/how I want to proceed with this
			     ;; :store (lambda (jf/org-link-store-link-for :scheme "abbr-plural"))
			     )

Date Entries

I want to register the date scheme for Org-Mode links.

(org-link-set-parameters "date"
			 :complete #'jf/denote-link-complete-date
			 :export #'jf/denote-link-export-date
			 :face #'denote-faces-link
			 :follow #'jf/denote-link-follow-date
			 ;; :store (lambda (jf/org-link-store-link-for :scheme "abbr"))
			 )

(cl-defun jf/denote-link-complete-date (&optional parg)
  "Prompt for the given DATE.

While we are prompting for a year, month, and day; a reminder
that this is intended to be conformant with the TIME element.
But for my typical use I write these as either years; years and
months; and most often year, month, and days."
  (format "date:%s" (org-read-date)))

(cl-defun jf/denote-link-export-date (link description format protocol)
  "Export a date for the given LINK, DESCRIPTION, FORMAT, and PROTOCOL."
  (cond
   ((or (eq format 'html)
	(eq format 'md))
    (concat "<time datetime=\"" link "\">" description "</time>"))
   (_ (format "%s (%s)" descirption link))))


(cl-defun jf/denote-link-follow-date (date &optional parg)
  (message "TODO, implement link for %s" date))

Conclusion

I wrote this configuration with the intention of publishing to my blog. I have locally tested things, a bit, but have not incorporated it into my dotemacs. That is a future concern.