Marking Up Key Sequences For HTML

The other day, I wrote about Xah Lee’s post on aliases. One of his aliases was for htmlize-keyboard-shortcut-notation. I realized that this was probably the function he used to markup the key sequences in his posts and I had (yet another) face-palm moment. As you may recall, when I decided to steal borrow his notation I implemented it with Org mode macros. There are a couple of problems with that. First, it’s a pain to type. To get the sequence 【⌘ Cmd+Tab】 I have to type

【{{{key(⌘ Cmd)}}}+{{{key(Tab)}}}】

I have to lookup the unicode value of the ⌘ symbol and enter it with the 【Ctrl+x 8】 key sequence. The same thing applies to the lenticular brackets that frame the key sequence.

Second, I have to include a file that contains the macro definition for every post I make that includes a marked up key sequence. Both of these problems are a result of my initial decision to use Org mode macros to enter the <span> tags that CSS uses to produce the fancy key representations.

When I saw Lee’s alias, I realized that the right way to do this is to enter Cmd+Tab when I want to have the sequence 【⌘ Cmd+Tab】 and then run some elsip code on it. That turns out to be reasonably easy to do.

The idea is to do repeated applications of replace-regexp-in-string with regular expressions and replacement strings such as

"Cmd""@<span class="key"> ⌘ Cmd@</span>"

It’s a pain to keep writing out all that <span> notation so we start with a helper macro

(defmacro key (k)
  "Convenience macro to generate a key sequence map entry
for \\[prettify-key-sequence]."
  `'(,k . ,(concat "@<span class=\"key\">" k "@</span>")))

With this macro, we can transform a key into a Lisp cons

(key "Tab") → ("Tab" . "@<span class=\"key\">Tab@</span>")

With the key macro, it’s pretty easy to write the function to do the markup:

 1:  (defun prettify-key-sequence (&optional omit-brackets)
 2:    "Markup a key sequence for pretty display in HTML.
 3:  If OMIT-BRACKETS is non-null then don't include the key sequence brackets."
 4:    (interactive "P")
 5:    (let* ((seq (region-or-thing 'symbol))
 6:           (key-seq (elt seq 0))
 7:           (beg (elt seq 1))
 8:           (end (elt seq 2))
 9:           (key-seq-map (list (key "Ctrl") (key "Meta") (key "Shift")
10:                              (key "Tab") (key "Alt") (key "Esc")
11:                              (key "Enter") (key "Return") (key "Backspace")
12:                              (key "Delete") (key "F10") (key "F11")
13:                              (key "F12") (key "F2") (key "F3")
14:                              (key "F4") (key "F5") (key "F6") (key "F7")
15:                              (key "F8") (key "F9")
16:                              ;; Disambiguate F1
17:                              '("\\`F1" . "@<span class=\"key\">F1@</span>")
18:                              '("\\([^>]\\)F1" .
19:                                "\\1@<span class=\"key\">F1@</span>")
20:                              ;; Symbol on key
21:                              '("Opt" . "@<span class=\"key\">⌥ Opt@</span>")
22:                              '("Cmd" . "@<span class=\"key\">⌘ Cmd@</span>")
23:                              ;; Combining rules
24:                              '("\+\\(.\\) \\(.\\)\\'" .
25:                                "+@<span class=\"key\">\\1@</span> @<span class=\"key\">\\2@</span>")
26:                              '("\+\\(.\\) \\(.\\) " .
27:                                "+@<span class=\"key\">\\1@</span> @<span class=\"key\">\\2@</span> ")
28:                              '("\+\\(.\\) " .
29:                                "+@<span class=\"key\">\\1@</span> ")
30:                              '("\+\\(.\\)\\'" .
31:                                "+@<span class=\"key\">\\1@</span>"))))
32:      (mapc (lambda (m) (setq key-seq (replace-regexp-in-string
33:                                       (car m) (cdr m) key-seq t)))
34:            key-seq-map)
35:      ;; Single key
36:      (if (= (length key-seq) 1)
37:          (setq key-seq (concat "@<span class=\"key\">" key-seq "@</span>")))
38:      (delete-region beg end)
39:      (if omit-brackets
40:          (insert key-seq)
41:        (insert (concat "【" key-seq "】")))))
42:  

The heart of the function is the key-sequence-map on lines 9–31. Most of the entries are generated with the key macro but some are more complicated and were hand generated. The entries on lines 17–19 take care of preventing F10 (for example) from being mistaken for F1. The two lines at 21 and 22 have a symbol attached so the key macro wouldn’t work. Finally, lines 24–31 take care of combining single letters with the more complex keys.

First, we get the symbol at the point or the region—if there is one—using the region-or-thing function that I wrote about earlier. Then the mapc at line 32 applies replace-regexp-in-string to the raw key sequence repeatedly using the substitution strings in key-squence-map. The code at lines 36 and 37 take care of the special case of a single key. Finally, the original raw key sequence is deleted and replaced with the marked up string. Unless omit-brackets is non-nil, the lenticular brackets are also added.

I don’t need to markup key sequences often enough to bind prettify-key-sequence to a special key sequence so I just made a simpler name for it with an alias:

(defalias 'pks 'prettify-key-sequence)

There are probably some weird key sequences that prettify-key-sequence won’t handle, but those can be done in pieces; that’s why the omit-brackets option is there. The key sequences in the last two posts were done using this function and so far I’m happy with it. One thing for sure: it’s a lot easier than using the Org mode macro as I did before.

This entry was posted in Programming and tagged . Bookmark the permalink.