Aug 2, 2023chatgpt

Example Post

If you're writing about code, your blog probably needs...

  • Syntax highlighting so your code blocks are easier for readers to parse
  • Line numbers so you can reference the code you're discussing
  • Line highlighting so you can draw attention to specific parts of code
  • Block titles so you can add additional context
  • You'd probably want colors that matched your website, right? What if you could use absolutely any VS Code theme?
  • Oh, and you'll need light and dark mode of course, because of reasons
  • And wouldn't it be great if you could do this without any runtime performance costs?

Sound good? Let's get started.

Example

Here is an example of what we will be creating today:

pages/blog/[slug].tsx
import { allPosts, type Post } from "contentlayer/generated"
import { type GetStaticProps, type InferGetStaticPropsType } from "next"
 
export const getStaticPaths = () => ({
  paths: allPosts.map((post) => ({ params: { slug: post.slug } })),
  fallback: false,
})
 
export const getStaticProps: GetStaticProps<{
  post: Post
}> = ({ params }) => ({
  props: { post: allPosts.find((post) => post.slug === params?.slug) },
})
 
export default function SinglePostPage(
  props: InferGetStaticPropsType<typeof getStaticProps>,
) {
  return (
    <div>
      <h1>{props.post.title}</h1>
    </div>
  )
}

How It Works

As discussed in a previous post, we're using Contentlayer to integrate MDX and manage our content. Today we'll use the Pretty Code plugin to add syntax highlighting to code blocks in our markdown posts.

Here is how it works:

  • At build time, Contentlayer transforms our markdown content into JSX. We can intercept this process and apply additional transformations using plugins.
  • The Pretty Code plugin searches markdown for code blocks, leverages VS Codes syntax highlighting engine to evaluate the code, and adds an inline style to each token based on the selected VS Code theme.
  • React and Next.js render the JSX and generate a static HTML version, ready to be served to the user.

Before syntax highlighting:

const multiply = (a, b) => a * b

After syntax highlighting:

const multiply = (a, b) => a * b

The code block above generates the following HTML:

<div>
  <pre>
    <code>
      <span class="line">
        <span style="color:#C678DD">const</span>
        <span style="color:#ABB2BF"> </span>
        <span style="color:#61AFEF">multiply</span>
        <span style="color:#ABB2BF"> </span>
        <span style="color:#56B6C2">=</span>
        <span style="color:#ABB2BF"> (</span>
        <span style="color:#E06C75;font-style:italic">a</span>
        <span style="color:#ABB2BF">, </span>
        <span style="color:#E06C75;font-style:italic">b</span>
        <span style="color:#ABB2BF">) </span>
        <span style="color:#C678DD">=&gt;</span>
        <span style="color:#ABB2BF"> </span>
        <span style="color:#E06C75">a</span>
        <span style="color:#ABB2BF"> </span>
        <span style="color:#56B6C2">*</span>
        <span style="color:#ABB2BF"> </span>
        <span style="color:#E06C75">b</span>
      </span>
    </code>
  </pre>
</div>

Why Is This a Big Deal?

  • Leveraging VS Code's ecosystem means highly accurate syntax highlighting and access to the entire VS Code theme catalog.
  • Generating the exact syntax colors at build-time means no runtime performance cost because there isn't any additional JavaScript or large CSS files sent to the client.
    • Methods that evaluate syntax and generate styles at runtime increase the JavaScript client-side bundle, produce flashes of unstyled code, and slow down the user's device.
    • Methods that use static CSS files either produce large files because they have to account for every possible language or have inaccurate highlighting because they rely on token pattern matching.

Install Pretty Code

  • Install Pretty Code and its dependencies
Terminal
npm install rehype-pretty-code shiki

Setup Pretty Code and Contentlayer

  • Add Pretty Code to the list of rehype plugins for MDX files.
./contentlayer.config.js
import { makeSource } from "contentlayer/source-files"
import rehypePrettyCode from "rehype-pretty-code"
import { Post } from "./content/definitions/Post"
 
export default makeSource({
  contentDirPath: "content",
  documentTypes: [Post],
  mdx: {
    rehypePlugins: [rehypePrettyCode],
  },
})

Create a Code Block

Let's create a post to preview our progress so far.

./content/posts/syntax-highlighting.mdx
# Code Block
 
This is my first code block:
 
```js
const multiply = (a, b) => a * b
```
  • Create a fenced code block in markdown by wrapping code in three backticks
  • Add a language annotation e.g. js or md to the code block to inform Pretty Code what syntax to use for highlighting

Customize Theme

We can customize the VS Code theme used to highlight our code.

./lib/rehypePrettyCode.ts
import { type Options } from "rehype-pretty-code"
import vercelLightTheme from "./lib/themes/vercel-light.json"
 
export const rehypePrettyCodeOptions: Partial<Options> = {
  // use a prepackaged theme
  theme: "one-dark-pro",
  // or import a custom theme
  theme: JSON.parse(vercelLightTheme),
}
  • Create a new file to store our Pretty Code customizations
  • Select a prepackaged theme (6), or import a JSON theme definition (2, 8)
./contentlayer.config.js
import { makeSource } from "contentlayer/source-files"
import rehypePrettyCode from "rehype-pretty-code"
import { Post } from "./content/definitions/Post"
+ import { rehypePrettyCodeOptions } from "./lib/rehypePrettyCode"
 
export default makeSource({
  contentDirPath: "content",
  documentTypes: [Post],
  mdx: {
    rehypePlugins: [
-     rehypePrettyCode
+     [rehypePrettyCode, rehypePrettyCodeOptions]
    ],
  },
})
  • To pass our configuration into the pretty code plugin, we can change the plugin definition to an array, where the first item is the plugin definition and the second is the options object.

Customizing Code Blocks

Using Annotations

We can enable features per code block using annotations. For example, we can set the language, enable line numbers, add a title, and highlight lines.

The following annotations:

```js showLineNumbers title="multiply.js" {3}
const multiply = (a, b) => a * b

multiply(2, 2) // 4
```

Become:

multiply.js
const multiply = (a, b) => a * b
 
multiply(2, 2) // 4

Using Styling

Outside of the syntax highlighting itself, Pretty Code doesn't come with any styling. We can use the data attributes it adds to code blocks to style elements.

The HTML structure and data attributes can be found by inspecting a code block in your browser's developers tools:

Example HTML output
<div data-rehype-pretty-code-fragment>
  <pre data-language="js" data-theme="default">
    <code data-language="js" data-theme="default">
      <span class="line">
        <span style="color:#C678DD">a</span>
        <span style="color:#ABB2BF">b</span>
        <span style="color:#61AFEF">c</span>
        <!-- [...] -->
      </span>
    </code>
  </pre>
</div>

Create a new css file:

./styles/syntax-highlighting.css
div[data-rehype-pretty-code-fragment] {
  /* ... */
}

And import it in your main Tailwind file:

./styles/globals.css
@tailwind base;
@import "./syntax-highlighting.css";
@tailwind components;
@tailwind utilities;

Code Block Container

Let's start by creating a styled wrapper for our code blocks.

./styles/syntax-highlighting.css
div[data-rehype-pretty-code-fragment] {
  overflow: hidden;
 
  /* stylist preferences */
  background-color: rgb(255 255 255 / 0.1);
  border-radius: 0.5rem;
}
 
div[data-rehype-pretty-code-fragment] pre {
  overflow-x: auto;
 
  /* stylist preferences */
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  font-size: 0.875rem;
  line-height: 1.5rem;
}
  • Hide overflow (2) and enable horizontal scrolling (10) for long lines of code

Block Titles

We can add title annotations to our code blocks using the title="..." syntax:

./content/posts/syntax-highlighting.mdx
```js title="multiply.js"
const multiply = (a, b) => a * b
```

And we can style those titles using the data-rehype-pretty-code-title data attribute:

./styles/syntax-highlighting.css
div[data-rehype-pretty-code-title] {
  /* stylistic preferences */
  margin-bottom: 0.125rem;
  border-radius: 0.375rem;
  background-color: rgb(255 228 230 / 0.1);
  padding-left: 0.75rem;
  padding-right: 0.75rem;
  padding-top: 0.25rem;
  padding-bottom: 0.25rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
    "Liberation Mono", "Courier New", monospace;
  font-size: 0.75rem;
  line-height: 1rem;
  color: rgb(255 228 230 / 0.7);
}

Lines

Pretty Code adds the .line class to each line of code. We can use this to add styling to our lines.

./styles/syntax-highlighting.css
div[data-rehype-pretty-code-fragment] .line {
  /* stylistic preferences */
  padding-left: 0.75rem;
  padding-right: 0.75rem;
}

Line Highlights

We can highlight different lines using the {<line number>} annotation e.g. {3}. Multiple lines and ranges are supported e.g {3,4-8}.

./content/posts/syntax-highlighting.mdx
```js title="multiply.js"
const multiply = (a, b) => a * b
```

We can change our .line styles to accommodation highlighted lines:

./styles/syntax-highlighting.css
div[data-rehype-pretty-code-fragment] .line {
  /* stylistic preferences */
  padding-left: 0.5rem
  padding-right: 0.75rem;
 
  border-left-width: 4px;
  border-left-color: transparent;
}
 
div[data-rehype-pretty-code-fragment] .line--highlighted {
  border-left-color: rgb(253 164 175 / 0.7);
  background-color: rgb(254 205 211 / 0.1);
}
  • (6,7): Add a left border with a transparent border to every line.
  • (3): Reduce the left padding to account for the "invisible" border
  • (11,12): Make highlighted lines stand out by adding a background color and a border color to the "invisible" line border.

While Pretty Code automatically adds a .line class to each line of code, it doesn't automatically add classes for highlighted lines. We can push line--highlighted to the list of line classes using the onVisitHighlightedLine() method.

./lib/rehypePrettyCode.ts
import { type Options } from "rehype-pretty-code"
 
export const rehypePrettyCodeOptions: Partial<Options> = {
  theme: "one-dark-pro",
  onVisitHighlightedLine(node) {
    node.properties.className.push("line--highlighted")
  },
}

If you preview your changes you will notice the highlighted line doesn't span the full width of the code block. This is because lines are wrapped in a span (inline) and not a div (block). I'd imagine this is not to interfere with the intrinsic indentation of pre elements).

Converting the code element to a grid layout ensures the spans fill the full width of a horizontally-scrollable code block.

./styles/syntax-highlighting.css
div[data-rehype-pretty-code-fragment] code {
  display: grid;
}

Line Numbers

Similarly, we can enable line numbers in a code block using showLineNumbers:

./content/posts/syntax-highlighting.mdx
```js title="multiply.js" showLineNumbers
const multiply = (a, b) => a * b
```

However, this simply adds a data attribute to indicate the option should be enabled and doesn't add any HTML elements to the code block.

Instead, we can make clever use of the special counter() CSS function to add line numbers to our code blocks.

./styles/syntax-highlighting.css
code[data-line-numbers] {
  counter-reset: lineNumber;
}
 
code[data-line-numbers] .line::before {
  counter-increment: lineNumber;
  content: counter(lineNumber);
  display: inline-block;
  text-align: right;
 
  /* stylistic preferences */
  margin-right: 0.75rem;
  width: 1rem;
  color: rgb(255 255 255 / 0.2);
}
  • (5): Add a :before pseudo element to house our line numbers
  • (6): Increment the lineNumber counter by 1 for every instance of the elements
  • (7): Populate the contents of the pseudo element with the current lineNumber value
  • (2): Reset the lineNumber counter each instance of a code block

Dark and Light Mode

There is a performance compromise with supporting theme switching. We need to generate and send two themes for every code block because syntax highlighting is generated at build-time, and theme switching happens on the client at run-time.

First, change the theme option from a value to an object with dark and light options:

./lib/rehypePrettyCode.ts
import { type Options } from "rehype-pretty-code"
 
export const rehypePrettyCodeOptions: Partial<Options> = {
  theme: {
    dark: "one-dark-pro",
    light: "solarized-light",
  },
}

Then use CSS to show and hide the light or dark theme depending on the user's selection.

./styles/syntax-highlighting.css
pre[data-theme="dark"] {
  color-scheme: dark;
}
 
@media (prefers-color-scheme: dark) {
  pre[data-theme="light"] {
    display: none;
  }
}
 
@media (prefers-color-scheme: light), (prefers-color-scheme: no-preference) {
  pre[data-theme="dark"] {
    display: none;
  }
}

We will discuss adding the actual functionality of a theme switcher in a future post.

Closing Thoughts

...