Skip to content

Adding Line Numbers and Code Highlighting to MDX


Created Feb 19, 2020 – Last Updated Sep 01, 2022

GatsbyMDX
Digital Garden

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. 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 on Gatsby’s documentation.

Install the necessary packages for this quick tip:

sh
npm install prism-react-renderer unist-util-visit@^2

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

Also create a CSS file:

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.js to add them to your site:

gatsby-browser.js
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.js at the root:

rehype-meta-as-attributes.js
js
const visit = require(`unist-util-visit`)
const re = /\b([-\w]+)(?:=(?:"([^"]*)"|'([^']*)'|([^"'\s]+)))?/g
function rehypeMetaAsAttributes() {
return (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
}
}
})
}
}
module.exports = rehypeMetaAsAttributes

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

gatsby-config.js
js
const rehypeMetaAsAttributes = require(`rehype-meta-as-attributes`)
// Rest of config...
{
resolve: `gatsby-plugin-mdx`,
options: {
// Rest of options...
mdxOptions: {
rehypePlugins: [rehypeMetaAsAttributes],
},
},
},

Switch to your file that contains the MDXProvider. This is most likely your layout file, check Defining a layout 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.

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