How to add syntax highlighting to HTML emails

If you want to show a block of code in an HTML email and have it look nice, it usually involves a lot of manual work: escaping, formatting, tokenizing, styling tokens...

With Maizzle however, we can use JavaScript libraries to do that work for us 💅

Getting started

Let's create a new Maizzle project.

Open a terminal window and run the new command:

maizzle new syntax-highlight

That will create a syntax-highlight folder at your current location, clone the Starter in it, and install NPM dependencies for you.

OK, now open the syntax-highlight folder in your editor.

Markdown

We'll use Markdown to add our code block to the email template.

We could manually add <pre> and <code> tags, but with Markdown the code is automatically formatted (escaped) for us. We can even add our own sanitizer function.

Edit your template and add a {% markdown %} tag somewhere in your content (anywhere a <pre> element would be allowed).

We'll be using the Promotional template from Maizzle, so we'll add the markdown block right after the <p> from the first article:

<p class="m-0 mb-24">For example, here's a block of JavaScript code:</p>
{% markdown %}
```js
function foo(bar) {
    var a = 42,
        b = 'Prism';
    return a + bar(b);
}
```
{% endmarkdown %}

Now run maizzle serve to start the development server, and open http://localhost:3000/promotional.html in a browser.

You'll see something like this:

Code without syntax highlighting

Not very pretty, is it?

Let's use PrismJS to make it look better.

PrismJS

Install the PrismJS library in your project by running this command in the terminal at your project's root:

npm install prismjs

Theme

We'll also need a PrismJS theme to make the code block pretty. Choose one of the default themes, or see prism-themes for more.

We'll go with the Synthwave '84 Theme, here's how it looks like:

Synthwave '84 theme preview

Save prism-synthwave84.css to the src/assets/css/custom directory in your project, and import it in your src/assets/css/main.css:

/* Your custom CSS resets for email */
@import "custom/reset";

/* Tailwind components that are generated by plugins */
@import "tailwindcss/components";

/**
 * @import here any custom components - classes that you'd want loaded
 * before the Tailwind utilities, so that the utilities could still
 * override them.
*/
@import "custom/prism-synthwave84";

/* Tailwind utility classes */
@import "tailwindcss/utilities";

/* Your custom utility classes */
@import "custom/utilities";

We'll get back to the prism-synthwave84.css file later.

Events

In order to use PrismJS to highlight fenced code blocks in our email template, we'll use Events in Maizzle. First, we need to require PrismJS, so edit config.js:

const Prism = require('prismjs')

module.exports = {
  // ...
}

Next, we'll be using the beforeCreate event, which allows us to programmatically set config options. Add it inside an events object, in config.js:

const Prism = require('prismjs')

module.exports = {
  // ...
  events: {
    async beforeCreate(config) {
      config.markdown.highlight = (code, lang, callback) => {
        return Prism.highlight(code, Prism.languages[lang], lang)
      }
    },
  },
}

Inside the beforeCreate() function, we define a highlight function for the Markdown renderer and have it use PrismJS for highlighting code blocks.

This function must return the highlighted code as an HTML string - take a look at the marked.js docs for an explanation of all the Markdown renderer options.

If we run maizzle build again, we see our code looking the same.

Why is it not working?

If you look at the source code, you'll see PrismJS did do something - it tokenized the code, which is just a fancy way of saying it wrapped pieces of text in <span> tags.

We don't notice any difference, because of CSS purging and clean-up in Maizzle.

Since we're not actually using any of them in our .njk template file, CSS classes from prism-synthwave84.css are purged (ignored).

We can fix this by telling the library that does this purging (postcss-purgecss) to ignore this file, by wrapping its contents in comments, like so:

/*! purgecss start ignore */
  /* contents of the prism-synthwave84.css file here... */
/*! purgecss start ignore */

Now, running maizzle build will finally show us we're making some progress:

Partial syntax highlighting

We're almost there, we just need to customize the Synthwave '84 theme a bit.

With PrismJS, we don't really need any class="language-xxxx", so we can remove those selectors from the CSS. Take a look at the final prism-synthwave84.css file.

Compatibility

Some email clients require extra steps in order to render our code blocks properly.

Gmail

Gmail will change our inline white-space: pre; to white-space: pre-wrap;. This results in code wrapping, instead of showing a horizontal scrollbar.

Fix it by adding the following CSS at the beginning of prism-synthwave84.css:

@screen all {
  pre {
    @apply whitespace-pre !important;
  }
}

Outlook

Padding on <pre> doesn't work in Outlook, and we'll also see a line-height issue.

We can fix these by wrapping {% markdown %} tags inside a table that we only show in Outlook. We then style this table inline, like so:

<!--[if mso]><table width="100%"><tr><td style="background: #2a2139; padding: 24px;"><![endif]-->
{% markdown %}
```js
function foo(bar) {
    var a = 42,
        b = 'Prism';
    return a + bar(b);
}
```
{% endmarkdown %}
<!--[if mso]></td></tr></table><![endif]-->

Inline code blocks

We can adjust the Synthwave '84 theme to style inline code, too.

Change this:

/* Inline code */
:not(pre) > code[class*="language-"] {
  padding: .1em;
  border-radius: .3em;
  white-space: normal;
}

to this:

/* Inline code */
:not(pre) > code {
  @apply bg-gray-100 border border-solid border-gray-300 text-red-400 text-sm px-8 py-4 rounded whitespace-normal;
}

Production build

We've been working with config.js until now, which is configured for local development. This means CSS isn't inlined, and most email optimizations are off.

When you're satisfied with the dev preview, run maizzle build production and use the build_production/promotional.html file for sending.

Resources