Skip to content

Adding line numbers and code highlighting to MDX code blocks

19/02/20203 min readCategory: Quick tip

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. The cherry on top is that @loadable/component is used to lazy-load 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 @loadable/component mdx-utils prism-react-renderer

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

Also create a CSS file:

src/components/layout.css
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;
}

Switch to your file that contains the MDXProvider. If you use e.g. the defaultLayouts option:

defaultLayouts: {
  default: require.resolve("./src/components/layout.js"),
},

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:

src/components/layout.js
import 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

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

Add the following to said component:

src/components/Code.js
import React from "react"
import loadable from "@loadable/component"
import theme from "prism-react-renderer/themes/nightOwl"

const LazyHighlight = loadable(async () => {
  const Module = await import(`prism-react-renderer`)
  const Highlight = Module.default
  const { defaultProps } = Module
  return props => <Highlight {...defaultProps} {...props} />
})

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 (
    <LazyHighlight
      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>
      )}
    </LazyHighlight>
  )
}

export default Code

You could replace the LazyHighlight block with regular imports. The syntax looks a bit funky as you have to get the default export from prism-react-renderer and the named export defaultProps.
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.

Tagged with
All Tags

Sparked your interest? Read all posts in the category Quick tip

More posts