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. 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 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.
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 for this to work.
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.
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
.