Mazzarolo MatteoMazzarolo 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:

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

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:

It works pretty nicely; Check it out:

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`;
 
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:

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:

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;
}
/**
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: