Adding Line Numbers and Code Highlighting to MDX

lekoarts

Lennart

Posted on August 6, 2021

Adding Line Numbers and Code Highlighting to MDX

In this very short quick tip you'll learn how to set up code blocks in MDX and Gatsby that support line numbers and code highlighting using the code renderer prism-react-renderer. A preview can be found on CodeSandbox.

First, make sure that you have a MDX blog set up. If you have that already, you can skip to the packages. If not, you should first read Getting Started with MDX on Gatsby's documentation.

Install the necessary packages for this quick tip:

npm install mdx-utils prism-react-renderer
Enter fullscreen mode Exit fullscreen mode

Create a Code React component in src/components/code.js and leave the file empty for now.

Also create a CSS file:

html,
body {
  margin: 0;
  padding: 0;
}

html {
  font-family: sans-serif;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

.prism-code {
  font-size: 1rem;
  padding-top: 1rem;
  padding-bottom: 1rem;
  -webkit-overflow-scrolling: touch;
  background-color: transparent;
  overflow: initial;
}

.token {
  display: inline-block;
}

p > code,
li > code {
  background: rgb(1, 22, 39);
  color: rgb(214, 222, 235);
  padding: 0.4em 0.3rem;
}

.gatsby-highlight {
  font-size: 1rem;
  position: relative;
  -webkit-overflow-scrolling: touch;
  overflow: auto;
}

gatsby-highlight > code[class*="language-"],
.gatsby-highlight > pre[class*="language-"] {
  word-spacing: normal;
  word-break: normal;
  overflow-wrap: normal;
  line-height: 1.5;
  tab-size: 4;
  hyphens: none;
}

.line-number-style {
  display: inline-block;
  padding-left: 1em;
  padding-right: 1em;
  width: 1.2em;
  user-select: none;
  opacity: 0.3;
  text-align: center;
  position: relative;
}

.highlight-line {
  background-color: rgb(2, 55, 81);
  border-left: 4px solid rgb(2, 155, 206);
}

.highlight-line .line-number-style {
  opacity: 0.5;
  width: calc(1.2em - 4px);
  left: -2px;
}
Enter fullscreen mode Exit fullscreen mode

Switch to your file that contains the MDXProvider. If you use e.g. the defaultLayouts option inside gatsby-plugin-mdx of gatsby-config.js:

module.exports = {
  plugins: [
    {
      resolve: "gatsby-plugin-mdx",
      options: {
        defaultLayouts: {
          default: require.resolve("./src/components/layout.js"),
        },
      },
    },
  ],
}
Enter fullscreen mode Exit fullscreen mode

You'll need to add the MDXProvider and the rest of the following code (it's essentially the components placed into the wrapping MDXProvider) to your Layout file:

import * as React from "react"
import { MDXProvider } from "@mdx-js/react"
import { preToCodeBlock } from "mdx-utils"
import Code from "./code"
import "./layout.css"

const components = {
  pre: (preProps) => {
    const props = preToCodeBlock(preProps)
    if (props) {
      return <Code {...props} />
    } else {
      return <pre {...preProps} />
    }
  },
}

const Layout = ({ children }) => (
  <MDXProvider components={components}>
    <div style={{ margin: "0 auto", maxWidth: 960, padding: "2rem" }}>
      {children}
    </div>
  </MDXProvider>
)

export default Layout
Enter fullscreen mode Exit fullscreen mode

The important bit is that you pass components into the provider and the previously created Code React component is used.

Add the following to said component:

import * as React from "react"
import Highlight, { defaultProps } from "prism-react-renderer"
import theme from "prism-react-renderer/themes/nightOwl"

const RE = /{([\d,-]+)}/

const calculateLinesToHighlight = (meta) => {
  if (!RE.test(meta)) {
    return () => false
  }
  const lineNumbers = RE.exec(meta)[1]
    .split(`,`)
    .map((v) => v.split(`-`).map((x) => parseInt(x, 10)))
  return (index) => {
    const lineNumber = index + 1
    const inRange = lineNumbers.some(([start, end]) =>
      end ? lineNumber >= start && lineNumber <= end : lineNumber === start
    )
    return inRange
  }
}

const Code = ({ codeString, language, metastring, ...props }) => {
  const shouldHighlightLine = calculateLinesToHighlight(metastring)

  return (
    <Highlight
      {...defaultProps}
      code={codeString}
      language={language}
      theme={theme}
      {...props}
    >
      {({ className, style, tokens, getLineProps, getTokenProps }) => (
        <div className="gatsby-highlight" data-language={language}>
          <pre className={className} style={style}>
            {tokens.map((line, i) => {
              const lineProps = getLineProps({ line, key: i })

              if (shouldHighlightLine(i)) {
                lineProps.className = `${lineProps.className} highlight-line`
              }

              return (
                <div {...lineProps}>
                  <span className="line-number-style">{i + 1}</span>
                  {line.map((token, key) => (
                    <span {...getTokenProps({ token, key })} />
                  ))}
                </div>
              )
            })}
          </pre>
        </div>
      )}
    </Highlight>
  )
}

export default Code
Enter fullscreen mode Exit fullscreen mode

The calculateLinesToHighlight helper function gets the metastring as an input which is the notation for the line highlighting. As you can see in the linked CodeSandbox example you can write {1,9-12} to highlight the first and 9th to 12th line.

💖 💪 🙅 🚩
lekoarts
Lennart

Posted on August 6, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related