Jonathan Lam

EE/CS @ The Cooper Union


Blog

pugjs shenanigans

On Tue Apr 20 2021 15:54:03 GMT-0400 (Eastern Daylight Time)

Return to blog

I recently wrote about how this website's build system is implemented. One of the more-or-less arbitrary design choices was to use the Pug (formerly Jade) templating engine. A templating engine is used to generate HTML from template files, which allow you to incorporate some scripting logic. In my case, it helps greatly with the generating all the static HTML for this website on the server-side: it removes a lot of the redundancy, performs some nice tasks like a simple minification, and allows for some nice scripting tasks like those I'll discuss here.

There are a good number of templating engine options. Some popular ones I'm aware of are EJS (an ERB-like Javascript solution), Mustache, Handlebars (an extension to Mustache but popular on its own), and Liquid (the Ruby-based engine used in Jekyll). There are also templates used in frameworks like Angular and React that are deeply integrated into their frameworks (i.e., Angular's components and directives, and React's JSX). There are probably more, but I'm not too knowledgeable or up-to-date in the subject -- this post will focus on endeavors in Pug specifically. Personally, I've used Jade and Handlebars in the past, and I like the SASS-like simplicity of Jade/Pug (the other options adhere more strongly to the traditional XML-like syntax, which feels tedious to me). The ultimate choice of Pug was arbitrary.

Including files: a common use case

From this point on, everything will refer exclusively to Pug. However, it is not limited to Pug, and you can probably do many of the same things in other templating engines.

Pug is the HTML equivalent of SASS. They both:

You don't need much to have a fairly powerful syntax. At the beginning, all this website required was the use of templates so that the template wouldn't have to be copied onto each page1.

// layout.pug
doctype html
html
    head
	link(rel='icon' href='favicon.ico')
    body
        h1 This is a common header

        block content
// index.pug
extends layout

block content
    This is the homepage!

So when we render index.pug, it'll automatically include all the code from the layout template and include the block content where the placeholder is. Pretty simple.

This extends nicely for multiple levels of subclassing. This allows the creation of "sublayouts." In the case of this website, I needed a overall layout template for the website, and a layout for all the blog posts that also follows the overall layout template. Then we have:

// blogpostlayout.pug
extends layout

block content
    h1= post.title
    block post_content
// aspecificblogpost.pug
extends blogpostlayout

block post_content
    p This is a blog post
    p Lorem ipsum ...

This simple setup sufficed for the first few posts, but very soon I began to wonder if I could do better.

Motiviation for Mixins

One task that I had to do in a few blog posts (such as this one) is to create code snippets. This requires loading an extra Javascript and CSS files, so I would rather not include it in every blog post, but only the blog posts that require it. One way to do this is to dump the code in another pug file and simply include this. In the case of using prism.js (as is the case for this website):

// mixins/syntax_highlighter.pug
script(src='path/to/prism.js')
link(rel='stylesheet' href='path/to/prism.css')
style // custom extra CSS styles
// someblogpost.pug
extends blogpostlayout

block post_content
    include mixins/syntax_highlighter

    pre
        code(class='language-matlab').
            clc; clear; close all;

This works, but there are still a few pain points. Having to manually include the required mixin files (by name) from each blog post is both brittle and inconvenient, as is the hardcoding in the specific code format required by prism.js. We can move the include statements into the blog post layout file and abstract library-specific code to mixins. This has the effect of shortening code, making it less brittle to library changes, and decreasing redundancy. This looks something like:

// mixins/syntax_highlighter.pug
mixin codestyle
    script(src='path/to/prism.js')
    link(rel='stylesheet' href='path/to/prism.css')
    style // custom extra CSS styles

mixin precode(language)
    pre
        code(class='language-' + language).
            block
// blogpostlayout.pug
extends layout

block content
    include mixins/syntax_highlighter

    h1= post.title
    block post_content
// someblogpost.pug
extends blogpostlayout

block post_content
    +codestyle

    +precode('matlab').
        clc; close all; clear;

Much cleaner. We've abstracted extra functionality to mixins that conditionally include the library (+codestyle) and generate library-like code (+precode). Now, hypothetically, were we to switch from prism.js to highlight.js, it would be almost effortless. This also incurs no cost to the user, as the generated code is the same, and libraries are conditionally included.

This demonstrates a painless design pattern that can be used for any library or non-library reusable components. My blog post layout file currently has the following includes:

include mixins/codestyle
include mixins/imglink
include mixins/mathjax
include mixins/footnote

where codestyle is for syntax highlighting with prism and mathjax is for including the MathJaX library. imglink and footnote are custom reusable components.

Automatic footnotes: a mixin case study

In an earlier post I found the need for footnotes2. I did this manually at first:

// someblogpost.pug
// ...
p Lorem ipsum dolor sit amet1, consectetur ...
// ...
p
    sup 1 Dummy text.

But of course this is very tedious to do, especially if you have to renumber something. The coupling of footnote indicator and footnote is very loose -- it's all just messy.

It would be great if we could have LaTeX-like citations: something like:

% loremipsum.tex
Lorem ipsum dolor sit amet\footnote{Dummy text}, consectetur ...

The context is kept close to the comment, and all is well. My first approach was the following:

// mixins/footnote.pug
-let footnotes = [];

// used to create an auto-numbered footnote
mixin footnote
    -footnotes.push(block);
    sup= footnotes.length

// called by blogpostlayout to dump footnote at bottom
mixin print_footnotes
    each footnote, fid in footnotes
        p
            sup= (fid+1) + '. ' + footnote
// blogpostlayout.pug
// ...
include mixins/footnote
// ...

block post_content
+print_footnotes
// someblogpost.pug
// ...
Lorem ipsum dolor sit amet#[+footnote Dummy text], consectetur ...

This is the general idea. We see again here the implicit block keyword used in the mixin, which is used to store the tag content of the mixin. I naively attempted to store that block variable into the array footnotes and print it out.

As things turn out, block doesn't simply get text-replaced with the rendered tag content. It actually ends up evaluating to a function that outputs something along the lines of:

pug_html += '
The tag contents
'; pug_html += '
as a series of
'; pug_html += '
lines like this
';

It seems pug_html is a global string in which the entire compiled HTML is (linearly) built. Taking inspiration from a CodePen by Velichko Konstantin3, we can hijack the outputted code by modifying the mixin like so:

// footnote.pug
mixin footnote
    // save prior generated HTML
    -const old_buf = pug_html;
    -pug_html = '';

    // emit lines of code that append to pug_html
    block

    // save hijacked code
    -footnotes.push(pug_html);

    // restore normal operation
    -pug_html = old_buf;

    // emit footnote indicator superscript (auto-numbered)
    sup= footnotes.length

This works as originally expected.

At some later point, I decided that it would be cool for each footnote indicator superscript to link to its respective footnote and vice versa, Wikipedia style, and it was trivial to add this into the mixin.

And that's it! I was surprised at how fun and extensible HTML could feel with Pug.

1. Inconsequential note: I use tabs rather than spaces in my source code, but the examples are displayed with four spaces for the sake of narrow screens.

2. Like this

3. Alternatively, there is some discussion on GitHub and a more verbose workaround here. I think the CodePen example is more elegant.


© Copyright 2021 Jonathan Lam