Syntax highlighting with Atom Highlights

How to integrate build-time or server-side syntax highlighting for markdown code fences with two libraries: markdown-it and Highlights (Atom’s syntax highlighting engine).

Syntax highlighting for websites generally works by tokenizing a string of source code and marking up each discrete scope of syntax. Each of these scopes is given HTML classes to identify it, and the resulting string of HTML divs and spans is then styled with CSS. When we see colored syntax highlighting, what we’re looking at is the result of this markup and styling.

It’s easy to use client-side scripts to process and mark up source code in-browser, but for reasons I’ll cover below, it’s more efficient and rewarding to do this on a server or during a build process, only relying on the browser to color the already-processed syntax with CSS.

Let’s dive right into the JavaScript:

import Highlights from 'highlights';
import MarkdownIt from 'markdown-it';
 
// Set up a Highlights instance:
const highlighter = new Highlights({
  // Prefix all highlighted scope classes with the same 'syntax--' string Atom does:
  scopePrefix: 'syntax--',
});
 
// Our highlighing function:
const highlight = (contents, lang) => {
  return highlighter.highlightSync({
    // Here's where we provide the source code we want highlighted:
    fileContents: contents,
    // Setting a file name is how Highlights determines the code's language.
    // We're faking this, because each code fence is effectively its own file:
    filePath: `fake.${lang}`,
  });
};
 
// Set up a MarkdownIt instance, configuring a few options.
// This is where we specify the highlight function to run:
const md = new MarkdownIt({
  // The following two options are configurable as you see fit:
  // (Refer to markdown-it's documentation.)
  html: true,
  typographer: true,
  // Set the highlight function, called on the contents of each code fence:
  highlight,
});
 
// And finally, here's a function to render markdown file contents to HTML:
// (Pass it a string, and it will return a string.)
const renderMarkdownToHtml = markdown => {
  return md.render(markdown);
};

Now we can call renderMarkdownToHtml() on our markdown file contents, and it will output a string that is the HTML rendering of that markdown, including syntax highlighting for any markdown code fence.

I’m not going to assume we’re using a specific rendering pipeline, but it’s safe to say this is straightforward to integrate with a Node server, or for static sites, with any task runner you choose.

It’s important to note Highlights only adds the divs, spans, and classes necessary to style code. (Use your browser’s DOM inspector to check out what this page’s code blocks look like.) We then need to add whatever CSS we want in order to provide visible syntax highlighting. On this site, I’m currently using GitHub Atom Light Syntax.

Finally, let’s take a look at some example markdown and highlighted output:

# Specify a language file extension following three backticks:
```js
// Then all code inside that block is highlighted using that language grammar:
const echo = str => console.log(str);
```
// Then all code inside that block is highlighted using that language grammar:
const echo = str => console.log(str);

Why this approach?

  • More/better language support: Because we don’t need to send a highlighting script to the client, this allows us to use the full-featured Highlights engine to highlight a broad set of languages during a build step. Any language not supported by Highlights, you can almost certainly find a third-party grammar for. If we chose to highlight client-side, we would only be able to send and render a limited subset of languages. Many client-side highlighting engines don’t support new or obscure languages to any extent.
  • Reusing Atom themes and knowledge: By using this full-fledged highlighting library, we benefit from the opportunity to learn from or reuse CSS written for high-quality Atom syntax themes.
  • Page performance: If you integrate highlighting into a static site’s build process, it will result in faster (and more conscientious) pages than offloading it to be executed by every client each time a page is loaded. This is the most compelling reason for me to go through the small amount of effort to integrate highlighting in my own build process.
Related articles

Beyond allowing the await keyword to be used within them, JavaScript’s async functions have their own useful aspects. As an alternative to Promise.resolve() and Promise.reject(), consider using async functions to cleanly wrap return values in Promises for convenient mocks in your test suite and to ensure consistent return types.

2018
JavaScript
TypeScript

A Firefox issue where right click on a button results in a click event listener firing because of event delegation performed automatically by a JavaScript SPA framework.

2017
Firefox
JavaScript

React is a go-to for single page applications. Is it also the right choice for isolated JavaScript components within traditional websites?

2017
React
JavaScript