Skip to content

Adding Line Numbers and Code Highlighting to MDX


Created: Feb 19, 2020 – Last Updated: May 23, 2023

Tags: Gatsby, MDX

Digital Garden

In this very short quick tip you’ll learn how to set up code blocks in MDX (opens in a new tab) and Gatsby that support line numbers and code highlighting using the code renderer prism-react-renderer (opens in a new tab). You can also combine this with the tip on Adding Language Tabs.

You’ll be able to write the following in your MDX:

mdx
```js highlight=1,3-5
const foo = "bar"
const hello = () => {
return "World"
}
```

The first and third to fifth line will be highlighted.

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 (opens in a new tab) on Gatsby’s documentation.

Install the necessary packages for this quick tip:

sh
npm install prism-react-renderer unist-util-visit

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

Also create a CSS file at the root of the project:

styles.css
css
.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;
}

Import the styles.css file into gatsby-browser.jsx to add them to your site:

gatsby-browser.jsx
js
import "./styles.css"

Next, create a rehype plugin to add the highlight information to the meta field of MDX. Then, those meta fields will be added as props that that you then can access.

Create a file called rehype-meta-as-attributes.mjs at the root:

rehype-meta-as-attributes.mjs
js
import { visit } from "unist-util-visit"
const re = /\b([-\w]+)(?:=(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g
const transformer = (tree) => {
visit(tree, `element`, (node) => {
let match
if (node.tagName === `code` && node.data && node.data.meta) {
re.lastIndex = 0 // Reset regex.
while ((match = re.exec(node.data.meta))) {
node.properties[match[1]] = match[2] || match[3] || match[4] || true
}
}
})
}
const rehypeMetaAsAttributes = () => transformer
export default rehypeMetaAsAttributes

In your gatsby-config.mjs, import the newly created rehype-meta-as-attributes and use it inside mdxOptions.rehypePlugins:

gatsby-config.mjs
js
import rehypeMetaAsAttributes from "./rehype-meta-as-attributes.mjs"
// Rest of config...
{
resolve: `gatsby-plugin-mdx`,
options: {
// Rest of options...
mdxOptions: {
rehypePlugins: [rehypeMetaAsAttributes],
},
},
},

You must use ESM in Gatsby (opens in a new tab) for this to work.

Switch to your file that contains the MDXProvider. This is most likely your layout file, check Defining a layout (opens in a new tab) if you haven’t one already.

You’ll need to create a helper function called preToCodeBlock and define the components object. The preToCodeBlock parses the incoming props from the pre tag and returns a normalized object that later the Code component uses. Later you’ll define shortcodes (opens in a new tab).

src/components/layout.jsx
js
import * as React from "react"
import { MDXProvider } from "@mdx-js/react"
import Code from "./code"
const preToCodeBlock = (preProps) => {
if (preProps?.children?.type === `code`) {
const {
children: codeString,
className = ``,
...props
} = preProps.children.props
const match = className.match(/language-([\0-\uFFFF]*)/)
return {
codeString: codeString.trim(),
className,
language: match !== null ? match[1] : ``,
...props,
}
}
return undefined
}
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 MDXProvider and the previously created Code React component is used.

Add the following to said component:

src/components/code.jsx
js
import * as React from "react"
import Highlight, { defaultProps } from "prism-react-renderer"
import theme from "prism-react-renderer/themes/nightOwl"
const calculateLinesToHighlight = (meta) => {
if (!meta) {
return () => false
}
const lineNumbers = meta
.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, highlight, ...props }) => {
const shouldHighlightLine = calculateLinesToHighlight(highlight)
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

The calculateLinesToHighlight helper function gets the highlight prop from the preProps with the help of rehype-meta-as-attributes.


Want to learn more? Browse my Digital Garden