Client Side Syntax Highlight with PrismJS via Webpack

kinopyo avatar
kinopyo

PrismJS is a syntax highlighter. Chances are you may have seen code examples that are highlighted by it. In this post I'll show you how to install and run PrismJS on the client side via Webpack.

Skip to the installation section if you're in a hurry.

Why PrismJS?

Good-looking themes

When it comes to syntaxh highlighters, a good looking theme is worth a thousand words, right? 😀

PrismJS comes with a few common themes by default (Twilight, Solarized Light, Tomorrow Night, etc), and you can find more community-created themes at https://github.com/PrismJS/prism-themes. It's also easy to create one as PrismJS CSS classes are standarized and easy to be styled for.

Customizable & Extensible

PrismJS provides a handful of hooks that you can run your callbacks during the life cycle events:

# list of PrismJS hooks
- before-sanity-check
- before-highlight
- before-tokenize
- after-tokenize
- wrap
- before-insert
- after-highlight
- complete
// add callback to "complete" hook
Prism.hooks.add("complete", (env) => {
  const pre = env.element.parentNode
  pre.classList.add("my-class")
}

This mechanism makes it easy to customize and extend the behavior, such as adding a wrapper to the codeblock.

Powerful plugins

We all expect syntaxh highlighters to highlight the syntax correctly - that's the baseline, so it's those "one more thing..." that makes it shine. I find PrismJS plugins modern and convenient that it fulfills most of the common needs. Here are some highlights:

Autoloader

bloggie.io had been using highlight.js, and we needed to load all the languages upfront, which put us in a tough position as we had to balance between supporting more languages and keeping the JavaScript bundle size small.

Now with PrismJS, when users want to highlight language that are not in our bundle, the autoloader plugin can load it automatically from CDN or specified locations.

Line highlight

I didn't realize how much I, as a writer/blogger, would need this feature. But once it's there, I'm surprised how much more expressive it brings to the code snippet.

Copy-to-clipboard button

Cool kids all have it these days. 😎 With one line of configuration, then you get this feature out of the box.

You can find out more plugins at https://prismjs.com/#plugins. What we want to add to Bloggie later:

  • Line Numbers: Line number at the beginning of code lines
  • Command Line: Display a command line with a prompt to avoid copying the $ prompt
  • Diff Highlight: Highlights the code inside diff blocks
  • Tree View: Highlight file system tree structures

That's quite a lot of good things for PrismJS. Intrigued? Let's move to the next section and see how to install it with Webpack and Babel.

Install PrismJS with babel-plugin-prismjs

yarn add prismjs
yarn add -D babel-plugin-prismjs

babel-plugin-prismjs is highly recommended when using Prism with JS bunlders such as Webpack. The plugin makes it easy to configure languages and plugins you want from PrismJS, and it will handle the dependencies and load orders.

To elaborate on the it, both PrismJS languages and plugins have dependencies. For example, the copy-to-clipboard plugin depends on toolbar plugin, and the latter needs to be loaded beforehand. toolbar plugin also has its corresponding CSS file that need to be imported too. babel-plugin-prismjs allows you to configure what languages, plugins, and theme you want, and it'll handle the rest for you.

In your .babelrc, add the following code and customize the languages, plugins, and theme:

{
  "plugins": [
    [
      "prismjs",
      {
        "languages": [
          "javascript",
          "css",
          "markup"
        ],
        "plugins": [
          "copy-to-clipboard"
        ],
        "theme": "tomorrow",
        "css": true
      }
    ]
  ]
}

Then in your entry file:

import Prism from "prismjs";

babel-plugin-prismjs will complie the line above into:

import Prism from "prismjs/components/prism-core";
import "prismjs/components/prism-clike";
import "prismjs/components/prism-markup";
import "prismjs/components/prism-javascript";
import "prismjs/components/prism-css";
import "prismjs/plugins/toolbar/prism-toolbar.css";
import "prismjs/plugins/toolbar/prism-toolbar";
import "prismjs/plugins/copy-to-clipboard/prism-copy-to-clipboard";
import "prismjs/themes/prism-tomorrow.css";

As you can see, the highlighted code shows copy-to-clipboard dependencies are loaded automatically. See how it's implemented: https://github.com/mAAdhaTTah/babel-plugin-prismjs/blob/c85ad55a444cab84c09ba6565ecee445a1987dca/src/index.js#L13-L21.

A side note on Webpack and mini-css-extract-plugin

I set "css": true in .babelrc so Prism CSS files will be compiled by Webpack. mini-css-extract-plugin will extract the CSS into a "chunk" file like vendors~application.css. Make sure you've configured chunkFilename for mini-css-extract-plugin. I missed the setting and it took me quite a while to get tings work. 🤦‍♂️

plugins: [
  new MiniCssExtractPlugin({
    filename: PROD ? "[name]-[contenthash].css" : "[name].css",
    chunkFilename: PROD ? "[name]-[contenthash].css" : "[name].css"
  })
]

Then in my Rails application layout file, I also need to import the vendor CSS file:

<%= stylesheet_link_tag webpack_asset_path("vendors~application.css"), "data-turbo-track": "reload", media: "all" %>

webpack_asset_path is my custom helper method to find the webpack assets, your mileage may vary.

That's the installation! I'm assuming you have configured your webpack.config.js for babel, css-loader, style-loader / mini-css-extract-plugin.

PrismJS markups and APIs

The markup requirement is simple and adhere to HTML5 - all it needs is having language-xxx class on the <code> node.
(To be exact, you can see its implementation of all the selectors it supports.)

<pre>
  <code class="language-xxx">hello world</code>
</pre>

When PrismJS is imported, it finds all the containers that match the criteria and highlights them.

If you want to trigger the highlight manually:

import Prism from "prismjs";
Prism.manual = true;

There are a few APIs you can choose from:

// Highlight everything under `document`
Prism.highlightAll();

// Allow you to specify the container
Prism.highlightAllUnder(container);

// Highlight a single DOM `code` element that has a class of `language-xxx`
Prism.highlightElement(codeElement);

// Low-level function. It returns the string of the highlighted result.
// The only use case I had was to pair it with markdown-it or marked.js.
Prism.highlight("var foo = true;", Prism.languages.javascript, "javascript");

If you need to use the low-level Prism.highlight, you may want to save this snippet in your notes:

// https://github.com/PrismJS/prism/issues/1881#issuecomment-490052771
function highlight(code, language) {
  if (Prism.languages[language]) {
    return Prism.highlight(code, Prism.languages[language], language);
  } else {
    return Prism.util.encode(code);
  }
}

Check out prism-core.js source code to learn more about the APIs and what hooks they trigger. The code is easy to follow and documents are well written. 👍

Configure Autoloader plugin path

In practice, I've only included a few common languages in the .babelrc. If a post submitted by our users contains languages that are not included in the bundle, I'm relying on the autoloader plugin to load them from CDN.

import Prism from "prismjs";
Prism.plugins.autoloader.languages_path = "https://cdn.jsdelivr.net/npm/[email protected]/components/";

This is not ideal as when I upgrade Prism via yarn or npm, I'll need to update this URL as well. Let me know in the comment if there is a better way to configure autoloader in this context. 🙏

You can choose the CDN from cdnjs, jsDelivr, and UNPKG.

Extra random notes

  • When you import Prism, it is loaded into the global object, so it's available anywhere in your JavaScript files.
  • Auto detecting language is not supported: https://github.com/PrismJS/prism/issues/1313
  • File name is not supported out of the box. https://github.com/PrismJS/prism/issues/910 shows how to use show-language plugin to display the file name. Alternatively, you can also implement it yourself by adding a callback to "complete" hook.
  • PrismJS uses language-none for plain text, whereas highlight.js uses language-plaintext. So be careful in your migration script to update the markup correctly.

I found there are many articles about using PrismJS with Node (server side) but not that many on the client side. Adding and Customizing PrismJS in Your Webpack Theme Bundle - ian.pvd helped me get started, and I hope this post is helpful for you too. 🙂

kinopyo avatar
Written By

kinopyo

Indoor enthusiast, web developer, and former hardcore RTS gamer. #parenting
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.