Many Small Tools Make Light Work (in Emacs)

Stiching Together YASnippet, Hugo Short Codes, Shell Commands, and YAML Files in Emacs

Recently, I received heart warming words of thanks and encouragement from emsenn.net for my recent Emacs (Emacs 🔍) blog posts. See Toggling Keyboard Mapping for Org Roam and Revisiting Hydra Menu for Org Roam Lookup in Emacs.

I want to share a few more bits of tooling I use to assist in my writing. This involves the following:

  • The YASnippet package for Emacs
  • A Hugo (Hugo 🔍) shortcode
  • Some Elisp: dialect of Lisp used in GNU Emacs (Elisp 🔍)
  • A Yet Another Markup Language (YAML 🔍) file
  • The The Silver Searcher (ag 🔍), cut (Unix command) (cut 🔍), and sort (Unix command) (sort 🔍) command-line functions.

Example Walkthrough

Throughout my site you might read the following:

In my editor, I write those those three list items with the following markdown and Hugo shortcode.


* {{< linkToGame "swn" >}}
* {{< linkToSeries "new-vistas-in-the-thel-sector" >}}
* {{< abbr "swn" >}}

Anatomy of a Hugo Shortcode

A Hugo shortcode encodes a bit of rendering logic. It’s useful when multiple pages want to render the same concept but provide different options.

Note: I added line numbers to help discuss this.

Here is my custom linkToGame shortcode
 1  {{- $game := .Get 0 }}
 2  {{- $key := printf "linkToGame-%s" $game }}
 3  {{- $entry := index (where $.Site.Data.glossary "game" "eq" $game) 0 }}
 4  {{- if ($.Page.Scratch.Get $key) }}
 5    <cite>{{- $entry.title }}</cite>
 6  {{- else }}
 7    {{- with $entry.offer }}
 8      <cite><a href="{{- . }}">{{- $entry.title }}</a></cite>
 9    {{- else }}
10      <cite>{{- $entry.title }}</cite>
11    {{- end }}
12  {{- end }}
13  {{- .Page.Scratch.Set $key "t" }}

Let’s walk through each of the 13 lines. The actual code for this is 20 lines long and includes some additional markup. For the purposes of simplifying this post, I’ve shorted the shortcode.

Line 1: {{- $game := .Get 0 }}
Get the first positional parameter for this shortcode. In the above example, $game is the string swn.
Line 2: {{- $key := printf "linkToGame-%s" $game }}
Create a string (e.g. `linkToGame-swn`). Using Hugo's Scratch variable, I'll use this string as a means of knowing if I've previously rendered a link. If I use this shortcode more than once on a page, I only want to render a link the first time.
Line 3: {{- $entry := index (where $.Site.Data.glossary "game" "eq" $game) 0 }}
Lookup up given game in my personal glossary; Choose the first found instance and set that to the `$entry` variable. This leverages Hugo's Data Folder. My glossary contains several hundred concepts. I'll get into that later on the Glossary File.
In Ruby, the Glossary file is an Array of Hashes. In Golang, the Glossary file is a Map of Dictionaries.
Line 4: {{- if ($.Page.Scratch.Get $key) }}
This line of logic asks: "If we've already rendered a link to this game."
Line 5: <cite>{{- $entry.title }}</cite>
Since we've already rendered a link, don't render a link to the game. Instead cite the game by the glossary entry's title.
Line 6: {{- else }}
Else, if we haven't already rendered the link to this game, then do line 7 through 11.
Line 7: {{- with $entry.offer }}
Using Hugo's with function, if the given game's glossary entry has an `offer` attribute, then do line 8.
Line 8: <cite><a href="{{- . }}">{{- $entry.title }}</a></cite>
Render a link to a purchase offer for this game.
Line 9: {{- else }}
Else, if I don't have a URL, do line 10.
Line 10: <cite>{{- $entry.title }}</cite>
I don't have a link for the game, so cite the game's title instead.
Line 11: {{- end }
Close the with statement opened on line 7.
Line 12: {{- end }
Close the if statement opened on line 4.
Line 13: {{- .Page.Scratch.Set $key "t" }}
Record, via the scracth, that we've rendered this game via. Future calls to this shortcode within the same page will now answer line 4 as false.

The Glossary File

The following references YAML notation. The indentation matters, as does the - at the beginning of the first line. In this single Dictionary entry, there are 7 terms: title, key, itemid, offer, tag, game, and abbr. Each term has a single value (e.g. ‘Stars without Number: Revised Edition’, SWN, etc).

In line 3 of the linkToGame shortcode, we fetch the Dictionary (or Hash) that has swn the game key. Below is a glossary entry printed in YAML form.

- title: 'Stars without Number: Revised Edition'
  key: SWN
  itemid: https://www.wikidata.org/wiki/Q67963569
  offer: https://www.drivethrurpg.com/product/226996/Stars-Without-Number-Revised-Edition?affiliate_id=318171
  tag: swn
  game: SWN
  abbr: SWN

update: I extracted the glossary to my Hugo theme repository; Checkout the README for more information.

The additional keys help me maintain consistency reference the same concepts post after post. Examples include:

  • Using consistent abbreviations
  • Linking to disambiguation pages (e.g. Stars without Number has a Wikidata ID of Q67963569)
  • Consistently linking to offer URLs
  • Using the same title

The YASnippet for linkToGame

I use YASnippet to help in my writing. One of the snippets I have is to help with my linkToGame shortcode. Below is that snippet:


# -*- mode: snippet -*-
# contributor : Jeremy Friesen <jeremy@takeonrules.com>
# group: takeonrules
# name: {{< linkToGame >}}
# key: ltg
# --
{{< linkToGame "${1:$$(yas-choose-value (tor-game-list))}" >}}$0

That snippet is available in my blogging context. When I type ltg followed by the TAB key, the ltg expands into the following: {{< linkToGame "" >}}. The cursor positions between the two quote marks, and Emacs prompts me to select from the given list.

I generate that list in Emacs with the function named tor-game-list.

The tor-game-list Function

update: I added the fourth piped shell command (e.g. tr) to the Elisp function. In adding that fourth shell command, I can now account for spaces in my game entries.

Below is the Elisp code to generate the list for the YASnippet.

1 (defun tor-game-list ()
2  "Return a list of games from TakeOnRules.com."
3  (split-string-and-unquote
4   (shell-command-to-string
5    (concat
6     "ag \"game: .*$\" "
7     (f-join tor--repository-path "data/glossary.yml")
8     " -o --nofilename | cut -d \" \" -f 2- | sort" | tr '\n' '~'"))
9    "~"))

Let’s step through the function:

Line 1: (defun tor-game-list ()
Define the function named `tor-game-list`; This function has no input parameters.
Line 2: "Return a list of games from TakeOnRules.com."
A Docstring (Docstring 🔍) that describes the function.
Line 3: (split-string-and-unquote
A function that will split the STRING into a list of strings. In this case the STRING is the results of function call on line 4, and the optional separator (e.g. "`~`" is on line 9.)
Line 4: (shell-command-to-string
A function that will execute shell command COMMAND and return its output as a string. In this case the COMMAND is the result of line 5.
Line 5: (concat
A function that will concatenate all the arguments and make the result a string. In this case those arguments are lines 6, the result of line 7's function call, and line 8.
Line 6, 7, 8
The result of lines 6, 7, and 8 is ag "game: .*$" ~/git/takeonrules/data/glossary.yml -o --nofilename | cut -d " " -f 2- | sort | tr '\n' '~'. This is the command then run by line 4.
Line 9 is "~"))
We will split the results of line 4 on the "`~`" character.

The Shell Commands

I want to explain the shell command a bit more. I’ve separated the command into the four salient parts:

  1. ag "game: .*$" ~/git/takeonrules/data/glossary.yml -o ‐‐nofilename
  2. cut -d " " -f 2-"
  3. sort
  4. tr ‘\n’ ‘~'

First is the ag command.

The first parameter (eg. "game: .*$") is a regular expression. The regular expressions will find lines that have the phrase game: followed by a space, and any sequence of characters.

Looking back at my glossary file, this regular expression will only select the line game: swn.

The second parameter is the path to my glossary. The third parameter (eg. -o) says to only print the matching portions. The fourth parameter (eg. --nofilename) says to skip printing what file had the match.

, here's the results of that ag command.
game: awda
game: bwg
game: diaspora
game: dragonknights
game: dcc
game: dw
game: 5e
game: ll
game: lh
game: mhrpg
game: shsrd
game: sfad
game: swn
game: torchbearer
game: traveller
game: wwn

The second command (e.g. cut -d " " -f 2-) treats the entries like table. The -d " " switch identifies the column separator as an empty space (e.g. SPACE). The -f 2- switch tells the command to pick the second column and everything after. This is important if a game in my glossary were multiple words separated by spaces.

See the results of running the aforementioned cut command with the the above ag command output.
awda
bwg
diaspora
dragonknights
dcc
dw
5e
ll
lh
mhrpg
shsrd
sfad
swn
torchbearer
traveller
wwn

The third command (e.g. sort) alphabetizes the entries.

The fourth command (e.g. tr (Unix command) (tr 🔍)) will take the multi-line result, which is split by the \n character, and join those lines by with the ~ character.

Conclusion

I hope this rather extensive walkthrough highlights how to use different functional pieces to improve your writing workflows.

For myself, I write to learn and explore. In the process of writing, I’m also