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 (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:
```js highlight=1,3-5const foo = "bar"
const hello = () => { return "World"}```
The first and third to fifth line will be highlighted.
The following instructions require you to use at least gatsby@^5.3.0
, gatsby-plugin-mdx@^4.0.0
, and @mdx-js/react@^2.0.0
.
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:
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:
.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:
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:
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
The instructions were adapted from the MDX Guide Syntax Highlighting with the meta
field (opens in a new tab).
In your gatsby-config.mjs
, import the newly created rehype-meta-as-attributes
and use it inside mdxOptions.rehypePlugins
:
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).
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:
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
.