Dead Simple HTML Templating in Emacs Lisp
A lispy, one-function templating engine that has only a few key rules to remember. Use what you already know about Emacs Lisp to get you the rest of the way.
If you’ve ever needed to generate HTML from within Emacs, you know it can be a pain to use either buffer or string manipulation to build the markup from scratch. You know in your gut that it’s never a good idea to put code inside strings inside other code. There must be another way — and there is!
What if you could turn these Lisp s-expressions…
`(nav :class "main-menu" (ul (li "Home") (li :class "active" (b "About")) (li "Contact Us")) (button :class "search-button" (img :src "search-icon.svg" :alt "Magnifying glass icon")))
…into HTML like this?
<nav class="main-menu"> <ul> <li>Home</li> <li class="active"> <b>About</b> </li> <li>Contact Us</li> </ul> <button class="search-button"> <img src="search-icon.svg" alt="Magnifying glass icon"/> </button> </nav>
I wrote a single function, which I’m calling
charge-html, that does exactly that. It is a little long, so I’m pasting it at the end of this blog post1 for you to extend and play with later.
To use it, simply call it with a bunch of s-expressions in the same format as I showed you before:
(charge-html `(nav :class "main-menu" (ul (li "Home") (li :class "active" (b "About")) (li "Contact Us")) (button :class "search-button" (img :src "search-icon.svg" :alt "Magnifying glass icon"))))
Why did I make this?
I needed a HTML templating system to do some static content generation (like this blog). In researching that, I came across the Emacs package pp-html and found its core templating syntax really easy to understand.
However, I didn’t need all of pp-html’s shortcuts and fancy features, so I decided to write one extremely minimal function to cover just the basics of tag rendering. By backquoting and using the regular everday power of Emacs Lisp, I could get myself the rest of the way towards full template functionality without having to build any intrinsic “magic” into the system. It really is just 98% Emacs Lisp, without any shenanigans.
So I ended up with a system where I only need to remember 6 simple rules when templating.
Rule #1: Backquote the template
Since the template is defined as an S-expression with tag names in place of function calls, we can’t evaluate the expression as normal lisp. It’s more like a list of symbols.
(charge-html `(button ; "button" is not a function to be called! "Login"))
Quoting essentially makes the tag name evaluated “literally” as a symbol.
But why backquote instead of quoting? If you don’t need to evaluate any lisp at all in the template then a regular
' is fine, but you’re likely going to substitute variables into the template, so
` will make things easier down the road.
Here’s an example of variable substitution using
, (on the button label, in this case)
(let ((button-label "Login")) (charge-html `(button ; "button" is not a function to be called! ,button-label)))
Rule #2: Tags are first, but can be omitted
If the first item in a template expression list is a symbol, then a pair of tags will be generated around the content.
(charge-html `(p "The " "quick " "brown " "fox."))
<p>The quick brown fox.</p>
But it can also be omitted2 if all you need is the naked content.
(charge-html `("The " "quick " "brown " "fox."))
The quick brown fox.
Why would you need to do this? Allowing a list of things to be rendered without needing a tag name lets you expand lists of dynamic content into a container element without generating unnecessary tags. Here’s an example:
(charge-html `(p "Here are all the arabic numerals: " (0 1 2 3 4 5 6 7 8 9)))
<p>Here are all the arabic numerals: 0123456789</p>
Rule #3: Attributes are a plist of keywords
In Emacs, a keyword is a symbol that starts with a colon like
To add attributes to a tag, just name the keywords according to the attribute name that you want, and the value of the attribute comes right after the keywords. These values can be whatever you want.
(charge-html `(button :id "login-btn" :class "hero" "LOG IN NOW"))
<button id="login-btn" class="hero">LOG IN NOW</button>
You can even stuff really complex attribute names and values into the tag if you need to.
(charge-html `(div :data-custom-property "foo" :style "color:red;font-weight:bold;"))
<div data-custom-property="foo" style="color:red;font-weight:bold;"></div>
Rule #4: Standalone attributes need a nil value
Some HTML attributes like
checked don’t actually take a value, and they exist in the opening tag just on their own. In that case, the template needs to follow that keyword with a
(charge-html `(input :type "checkbox" :value "foo" :checked nil))
<input type="checkbox" value="foo" checked/>
If you still want a value on an attribute but it must be an empty string, then you’ll need exactly that.
(charge-html `(img :src "placeholder.png" :alt ""))
<img src="placeholder.png" alt=""/>
Rule #5: The rest is element content
Everything that comes after the tag name and attribute key-value pairs will be treated as the element’s inner content. Anything that supports being formatted as a string is fair game here.
(charge-html `(section :class "example" ;; anything after this point in the list is stringified "foo" 7 test (1 2 3)))
And of course, it’s completely optional in case you specifically need nothing in the element.
(charge-html `(a :name "test"))
Oh, and elements can have more template expressions as their content too (otherwise it would be a pretty useless templating engine, eh?)
(charge-html `(p "The " (b "quick") " brown fox"))
<p>The <b>quick</b> brown fox</p>
Rule #6: Use lisp for anything more fancy
If you need formatting, concatenating, joining, mapping, looping, conditionals, filtering, sorting, or any number of things templating engines usually provide, just use Emacs Lisp! It’s completely at your disposal, even within a template.3
(require 'seq) (let ((some-primes '(2 3 5 7 11 13 17 19))) (charge-html `(ol "Prime numbers under ten: " ,(mapcar (lambda (x) `(li ,x)) (seq-filter (lambda (x) (< x 10)) some-primes)))))
<ol>Prime numbers under ten: <li>2</li><li>3</li><li>5</li><li>7</li></ol>
- The template expression must be either quoted or backquoted so that symbols like
divaren’t called as functions in Emacs Lisp
- The first symbol in the list is the tag name of the element. If it isn’t present, the opening and closing tags will be omitted.
- Attributes follow the tag name as plist-like pairs of
:keywordattribute names and string attribute values. They are optional.
- If an attribute has no value (i.e. only its name should be rendered), then assign
nilas its value
- Element content is collected from all the arguments that come after the attribute pairs. It can be zero or more things that will
formatto a string, or even more nested template expressions.
- For anything more complicated, like non-constant values, variable substitution, conditional rendering, looping, advanced string formatting, etc, escape out of the backquote using
,@and Just Use Lisp. Evaluated Lisp can return any content or nested template expressions.
Next, a static website generator
charge-html is actually one of the cornerstones of my personal static website generator
charge.el, which I’m still actively developing. But to give you an idea of its power despite its simplicity, this whole blog is now generated using it!
Here’s the source code for
(require 'subr-x) (defun charge-html (&rest template) "Turns a list of TEMPLATE s-exps (tag :attr value ...content) into HTML." (let (tag attr-name (content (list)) (attrs (list))) (mapc (lambda (x) (cond ((and x (listp x)) (push (apply #'charge-html x) content)) ((and (not tag) x (symbolp x)) (setq tag x)) ((keywordp x) (setq attr-name x)) (attr-name (push (cons attr-name x) attrs) (setq attr-name nil)) (t (unless (null x) (push (format "%s" x) content))))) template) (let ((tag-is-void (memq tag '(area base br col embed hr img input link meta param track wbr)))) (concat (when tag (thread-last attrs (nreverse) (mapcar (lambda (attr) (format (if (cdr attr) " %s=\"%s\"" " %s") (substring (symbol-name (car attr)) 1) (cdr attr)))) (apply #'concat) (format (if tag-is-void "<%s%s/>" "<%s%s>") tag))) (unless tag-is-void (thread-last content (nreverse) (apply #'concat))) (when (and tag (not tag-is-void)) (format "</%s>" tag))))))
If you omit the tag name at the beginning, then you shouldn’t define any attributes or attribute values. I’m not sure what will happen if you do 🙈
You should probably put complex logic outside the template though, as usual. Code inside templates should be limited to basic presentation logic.