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)
Ah well, looks like I have singular/plural form duplicates already. Gah.

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.