Mazzarolo Matteo
Mazzarolo Matteo

Improving Prism diff syntax highlighting in Gatsby

By Mazzarolo Matteo


To highlight the code blocks of my blog, I’m using Prism. My blog is powered by Gatsby (yes, I know, I’m still using Gatsby in 2022), and I’m using the gatsby-remark-prismjs plugin to add syntax highlighting to code blocks in markdown files.

In my blog, I’m using a couple of tiny customizations to improve the syntax highlighting on diffs.
Here’s how they work.

Please note that I’ll use images instead of “real” code blocks. I’m doing it to avoid breaking the blog post if I update my syntax highlighting strategy in the future.

Highlighting diff lines

Prism default syntax highlighting for diffs is kind of… meh:

diff highlight before

In comparison, GitHub’s highlighting is way more intuitive imho, because it highlights the interested lines:

diff highlight github

Prism does offer a way to achieve the same result by adding a diff-highlight class to the code block… but it doesn’t work in Gatsby.

Luckily, a kind stranger submitted a PR to the Gatsby repo to implement this feature and even released it as an npm package to try it out: @jonsully/gatsby-remark-prismjs.

To make it work:

  • Use @jonsully/gatsby-remark-prismjs instead of gatsby-remark-prismjs.
  • In your gatsby-config.js, pass down options: { showDiffHighlight: true } to @jonsully/gatsby-remark-prismjs.
  • Update gatsby-browser.js to load Prism’ line highlighting stylesheet for diffs (import "prismjs/plugins/diff-highlight/prism-diff-highlight.css";).

It works pretty nicely; Check it out:

diff highlight after

By the way, since the PR is still open and is referencing an old version of Gatsby, instead of using @jonsully/gatsby-remark-prismjs I recommend using the standard gatsby-remark-prismjs, and patch it with patch-package. Here’s the patch I’m using on gatsby@4.2:

patches/gatsby-remark-prismjs 6.21.0.patch
diff --git a/node_modules/gatsby-remark-prismjs/index.js b/node_modules/gatsby-remark-prismjs/index.js
index 51a498b..3dd8d44 100644
--- a/node_modules/gatsby-remark-prismjs/index.js
+++ b/node_modules/gatsby-remark-prismjs/index.js
@@ -25,6 +25,7 @@ module.exports = ({
   noInlineHighlight = false,
   showLineNumbers: showLineNumbersGlobal = false,
   showInvisibles = false,
+  showDiffHighlight = false,
   languageExtensions = [],
   prompt = {
     user: `root`,
@@ -82,7 +83,7 @@ module.exports = ({
     // @see https://github.com/gatsbyjs/gatsby/issues/1486


-    const className = `${classPrefix}${languageName}${diffLanguage ? `-${diffLanguage}` : ``}`; // Replace the node with the markup we need to make
+    const className = `${classPrefix}${languageName}${diffLanguage ? showDiffHighlight ? `-${diffLanguage} diff-highlight` : `-${diffLanguage}` : ``}`; // Replace the node with the markup we need to make
     // 100% width highlighted code lines work

     node.type = `html`;

Improving diff selection

One thing that even GitHub could do better is to make the diff tokens (+, -, and !) not selectable:

diff selection github

I’m pretty sure that 99% of the time you start selecting a diff in a code block, you do not want to select the diff tokens (e.g., when you want to copy-paste a multi-line addition), right? The only exception I could think of is when the code block is a git diff (just like in the snippet above).

In my blog, I solved this issue with CSS. The idea is to:

  • Hide the first character of each line of a diff file. This ensures I don’t show an empty space before every line (diff files use that to hold the diff tokens).
  • On each line that would have a diff token, add that same token as an absolutely positioned (non-selectable) :after content. This allows me to show the token over the left padding of the code block.

Here’s the code (selectors are targeting class names added by Prism):

/**
We prefix all selectors with pre:not(.language-diff), to ensure we leave 
git diffs intact. 
But all other diffs (e.g., JavaScript, C++, etc...), will have 
the changes applied.
*/

/* Hide the diff tokens "column" of diff files code blocks (the first 
   character of each line). */
pre:not(.language-diff) .token.prefix.inserted,
pre:not(.language-diff) .token.prefix.deleted,
pre:not(.language-diff) .token.prefix.unchanged {
  user-select: none;
  color: transparent;
}

/* On each line that would have had a "+" or "-" token, add that same token
 as an absolutely positioned character to the left of the code block. */
pre:not(.language-diff) .token.prefix.inserted::after {
  content: "+";
  position: absolute;
  margin-left: -1rem;
  user-select: none;
  color: green;
}

pre:not(.language-diff) .token.prefix.deleted::after {
  content: "-";
  position: absolute;
  margin-left: -1rem;
  user-select: none;
  color: red;
}

/** Adjust the margins and paddings of diff codeblocks to ensure the
 highlighted row is not blocked by the artificial token created above. */
pre:not(.language-diff).diff-highlight > code .token.inserted:not(.prefix),
pre:not(.language-diff) > code.diff-highlight .token.inserted:not(.prefix) {
  margin-right: -1rem;
  margin-left: -1rem;
  padding-right: 1em;
  padding-left: 1rem;
}

pre:not(.language-diff).diff-highlight > code .token.deleted:not(.prefix),
pre:not(.language-diff) > code.diff-highlight .token.deleted:not(.prefix) {
  margin-right: -1rem;
  margin-left: -1rem;
  padding-right: 1em;
  padding-left: 1rem;
}

And here’s the final result: