Emacs Blogging: Insert Tag from YAML Frontmatter Posts
My blog posts here usually have a line like:
tags: [ swift, xcode, codesigning ]
For tags I don’t use a lot, I sometimes don’t remember how to write them. So I do the only sane thing – and go to my website’s list of tags and look for a match.
Got annoyed by that now after a couple of years :)
Extract All Tags from All Posts
Using rg
, I can match these lines and focus on group matches inside without piping the output through sed
or similar.
The regular expression that works good enough for me:
^tags:\s\[([a-zA-Z0-9 -_]+)\]
The first group here then returns " swift, xcode, codesigning "
for the example above with the right ripgrep incantation:
$ rg --context 0 \
--no-filename \
--no-heading \
--replace "\$1" \
-- <<regex here>>
This produces just the string inside the tags: [...]
brackets, without filename, and no empty lines in between.
Here’s the Emacs Lisp version to get these lines:
(defun ct/all-tag-lines ()
"Extract the array contents of YAML lines for `tags: [...]'."
(let ((project-dir (cdr (project-current)))
(regex "^tags:\\s\\[\\s*([a-zA-Z0-9 -_]+)\\s*\\]"))
(shell-command-to-string
(concat "rg --context 0 --no-filename --no-heading --replace \"\\$1\" -- " (shell-quote-argument regex) " " project-dir))))
Now time to clean this up and make this usable.
Transform the Extracted YAML Lines Into Filterable Lists in Emacs
Samples output from rg
is e.g.:
zettelkasten, reading, archive
calendarpasteapp
zettelkasten, writing, personal, craft
nv, zettelkasten, software, review
writing, productivity, quantified-self
I need to split the lines into individual tags and then remove duplicates like zettelkasten
.
-
Split string by lines, trimming whitespace:
(split-string "..." "\n" nil " ")
-
Split lines by comma and/or spaces to extract individual tags, dropping empty strings:
(split-string "..." "[, ]+" t " ")
-
Combined:
(split-string "..." "[, \n]+" t " \n")
To delete duplicates, delete-dups
does the trick:
(defun ct/all-tags ()
"Return a list of unique tags across all articles."
(delete-dups
(split-string (ct/all-tag-lines) "[, \n]+" t " \n")))
This returns an (unsorted) list of unique tag strings.
zettelkasten
reading
archive
calendarpasteapp
writing
personal
craft
nv
software
review
productivity
quantified-self
With that, I’m almost finished. I can pass this to completing-read
to get – well, the name reveals almost as much: – interactive completion for matches in this selection.
And I ultimately want to insert the match, not just produce a result programmatically.
So this is the “public”, i.e. user facing function I’m using:
(defun ct/insert-project-tag ()
"Select and insert a tag from YAML frontmatter tags in the project."
(interactive)
(insert (completing-read "Tag: " (ct/all-tags))))
Since I’m using the built-in project.el
package, I added a key binding to C-x p t (am actually using SPC p t in command mode) to insert a tag:
(define-key project-prefix-map (kbd "t") #'ct/insert-project-tag)
Up next, I’d maybe like to push this completion from a selection in the mode-line to completion-at-point
, i.e. to get suggestions and auto-completion in-place while I type.
Judging by the speed I implemented these things in the past, it should be ready by 2026.