7 min read

Syntax Highlight Code in Markdown

Style code blocks and highlight individual lines.

ByAmir Ardalan
Like
Share on X

Here is some fancy syntax highlighting using react-syntax-highlighter and react-markdown in Next.js!

tsx
1const Hello = () => { 2 3 return ( 4 <div> 5 Let's dive into syntax highlighting! 6 </div> 7 ) 8} 9 10export default Hello; 11

So how exactly is it done? Most info about this references a now deprecated way of doing this (renderer is no longer a thing). With a bit of digging through the docs here's how you can get code syntax highlighting in Next.js (and Typescript).

Install Dependencies

npm i react-markdown react-syntax-highlighter

If you're using TypeScript, you will also want the Types. The following command will add the types package as a development dependency:
npm i @types/react-syntax-highlighter -D

Configure the ReactMarkdown Component

I will assume you already have some page or component where you are processing Markdown. In the case of react-markdown you may have a component called Markdown.tsx that looks something like this:

tsx
1// Markdown.tsx 2 3import { FC } from 'react; 4import ReactMarkdown from 'react-markdown'; 5 6type MarkdownProps = { 7 markdown: string & { content?: string }; 8}; 9 10const Markdown: FC<MarkdownProps> = ({ markdown }) => { 11 12 return ( 13 <ReactMarkdown> 14 {markdown.content} 15 </ReactMarkdown> 16 ) 17} 18 19export default Markdown; 20

In this basic example, the ReactMarkdown component is being passed raw markdown with the prop markdown.content. You can change this to work for your project.

From here, let's simply add the components prop to ReactMarkdown. This gives us the ability to add SyntaxHighlighter in the next steps.

tsx
1// Markdown.tsx 2 3import { FC } from 'react; 4import ReactMarkdown from 'react-markdown'; 5 6type MarkdownProps = { 7 markdown: string & { content?: string }; 8}; 9 10const Markdown: FC<MarkdownProps> = ({ markdown }) => { 11 12 const MarkdownComponents: object = { 13 // SyntaxHighlight code will go here 14 } 15 16 return ( 17 <ReactMarkdown 18 components={MarkdownComponents} 19 > 20 {markdown.content} 21 </ReactMarkdown> 22 ) 23} 24 25export default Markdown; 26

Choose the Right SyntaxHighlighter Import for Your Project

The official documentation covers this quite well, but here's the tl;dr:

  • Unless you want a massive JS bundle on your markdown pages, you should only be using the Light Build. You will need to manually import the specific languages you are going to be syntax highlighting in your markdown.
  • If you're using Next.js, change the theme import from …/dist/esm/… to …/dist/cjs/…. Hopefully ESM is fully supported in Next.js soon.
  • If you want to syntax highlight TSX/JSX you must use Prism. Highlight.js does not support TSX or JSX.

For the rest of this guide, I will be using the Prism Light Build with cjs imports. I will add TSX, TypeScript, SCSS, Bash, Markdown, and JSON support. Simply add whichever languages you'd like to use. Check here for languages supported by Prism.

tsx
1// Markdown.tsx 2 3import { FC } from 'react; 4import ReactMarkdown from 'react-markdown'; 5import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter'; 6import tsx from 'react-syntax-highlighter/dist/cjs/languages/prism/tsx'; 7import typescript from 'react-syntax-highlighter/dist/cjs/languages/prism/typescript'; 8import scss from 'react-syntax-highlighter/dist/cjs/languages/prism/scss'; 9import bash from 'react-syntax-highlighter/dist/cjs/languages/prism/bash'; 10import markdown from 'react-syntax-highlighter/dist/cjs/languages/prism/markdown'; 11import json from 'react-syntax-highlighter/dist/cjs/languages/prism/json'; 12 13SyntaxHighlighter.registerLanguage('tsx', tsx); 14SyntaxHighlighter.registerLanguage('typescript', typescript); 15SyntaxHighlighter.registerLanguage('scss', scss); 16SyntaxHighlighter.registerLanguage('bash', bash); 17SyntaxHighlighter.registerLanguage('markdown', markdown); 18SyntaxHighlighter.registerLanguage('json', json); 19 20const Markdown: FC<MarkdownProps> = ({ markdown }) => { 21 22 type MarkdownProps = { 23 markdown: string & { content?: string }; 24 }; 25 26 const MarkdownComponents: object = { 27 // SyntaxHighlight code will go here 28 } 29 30 return ( 31 <ReactMarkdown 32 components={MarkdownComponents} 33 > 34 {markdown.content} 35 </ReactMarkdown> 36 ) 37} 38 39export default Markdown; 40

Configure the SyntaxHighlighter component and add Line Highlighting Functionality

Let's finally start adding code for our ReactMarkdown custom component. We will import a theme and parse-numeric-range for the line highlight logic.

tsx
1// Markdown.tsx 2 3... 4import rangeParser from 'parse-numeric-range'; 5import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism'; 6 7type MarkdownProps = { 8 markdown: string & { content?: string }; 9}; 10 11const Markdown: FC<MarkdownProps> = ({ markdown }) => { 12 13 const syntaxTheme = oneDark; 14 15 const MarkdownComponents: object = { 16 code({ node, inline, className, ...props }) { 17 const hasLang = /language-(\w+)/.exec(className || ''); 18 const hasMeta = node?.data?.meta; 19 20 const applyHighlights: object = (applyHighlights: number) => { 21 if (hasMeta) { 22 const RE = /{([\d,-]+)}/; 23 const metadata = node.data.meta?.replace(/\s/g, ''); 24 const strlineNumbers = RE?.test(metadata) 25 ? RE?.exec(metadata)[1] 26 : '0'; 27 const highlightLines = rangeParser(strlineNumbers); 28 const highlight = highlightLines; 29 const data: string = highlight.includes(applyHighlights) 30 ? 'highlight' 31 : null; 32 return { data }; 33 } else { 34 return {}; 35 } 36 }; 37 38 return hasLang ? ( 39 <SyntaxHighlighter 40 style={syntaxTheme} 41 language={hasLang[1]} 42 PreTag="div" 43 className="codeStyle" 44 showLineNumbers={true} 45 wrapLines={hasMeta} 46 useInlineStyles={true} 47 lineProps={applyHighlights} 48 > 49 {props.children} 50 </SyntaxHighlighter> 51 ) : ( 52 <code className={className} {...props} /> 53 ) 54 }, 55 } 56 57 ... 58} 59 60export default Markdown; 61
  • First we check for the existence of the metadata to prevent TypeScript errors if it's undefined.
  • Inside of the applyHighlights function, we declare RE as our Regex checker.
  • Then we declare a variable called metadata that removes any spaces from the object.
  • strlineNumbers checks the metadata against the Regex and ensures the object contains only numbers, hyphens, commas.
  • rangeParser() spits out an array of line numbers based upon the range we defined in markdown!
  • We then check which lines to highlight and apply a data attribute data="highlight" on the spans wrap the lines we defined.

We now have the ability to style data="highlight" with some CSS. I will leave this up to you, as it will be highly dependent on the look and style you are going for.

Controlling Line Highlighting in Your Markdown

So now that you have your component code in place for React-Markdown and SyntaxHighlighter, how do you actually highlight lines of code within your markdown?

markdown
1```tsx {3-4, 8} 2

Simply add curly braces with comma separated line numbers and/or ranges of line numbers!

Adding Custom CSS

You can easily target your ReactMarkdown and SyntaxHighlighter components like so. You may have to use some !important tags to target certain elements within the Syntax Highlighting, which is less than ideal, but it works.

The following example is CSS-in-JS (Emotion specifically). Notice the class names and refactor the following for your CSS setup. I plan to update this guide to use Tailwind CSS in the future.
tsx
1// Markdown.tsx 2... 3 4const Markdown: FC<MarkdownProps> = ({ markdown }) => { 5 6 ... 7 8 const MarkdownComponents: object = { 9 10 const styleMarkdown = css({ 11 '.codeStyle, pre, code, code span': { 12 // Your SyntaxHighlighter override styles here 13 }, 14 code: { 15 // Your general code styles here 16 }, 17 'pre code': { 18 // Your code-block styles here 19 }, 20 'h3 code': { 21 color: 'inherit' 22 }, 23 'span.linenumber': { 24 display: 'none !important' 25 }, 26 '[data="highlight"]': { 27 // Your custom line highlight styles here 28 }, 29 }) 30 31 code({ node, inline, className, ...props }) { 32 33 ... 34 35 return hasLang ? ( 36 <SyntaxHighlighter 37 className="codeStyle" 38 ... 39 > 40 {props.children} 41 </SyntaxHighlighter> 42 ) 43 } 44 } 45 46 return ( 47 <ReactMarkdown 48 ... 49 css={styleMarkdown} 50 > 51 {markdown.content} 52 </ReactMarkdown> 53 ) 54} 55 56... 57

Fixing Mobile Issues

There is a known issue with Prism where your CSS background color for the line highlight will not extend past the viewport. So when you scroll horizontally on a mobile screen, the highlighting abruptly ends.

There are a lot of over-engineered recommendations to fix it, including wrapping the pre tag in a container to apply more styles. The fix I came up with is hacky, yet elegant:

scss
1code: { 2 transform: translateZ(0); 3 min-width: 100%; 4 float: left; 5 & > span { 6 display: block; 7 } 8} 9

I had almost forgotten about floats but hey... it works 🤷

Removing Line Numbers

The lineNumber prop must be set to true or the line highlighting won't work at all. If you'd like to disable the line numbers, this should do the trick:

scss
1span.linenumber { display: none; } 2

A better solution would be to write some additional functions to accept more meta strings for doing things like setting titles, disabling/enabling line numbers, etc. but I'll leave that for another day (and a future guide).

Whew, there you have it. Syntax highlighting your markdown in React using TypeScript with custom line highlighting. 🙌

Did you enjoy this post?

Like
© 2024 Amir Ardalan0 views