Typescript + Nextjs + Prismjs or the tale about code highlighting
I needed to add code highlighting to posts (no one likes ugly code :).
A quick googling showed that for my stack there is nothing ready. But everything revolves around Prismjs.
The guys from FormidableLabs with prism-react-renderer came
closest to what I need. But I want to figure it out myself.
update #1
to reduce js bundle size you can use babel-plugin-prismjs. With
this babel plugin you can bundle only languages you need.
You can find usage example here
If you didn’t come here for details:
Step 1:
yarn add prismjs
yarn add @types/prismjs -D
Step # 2:
in pages / _app.tsx add
import 'prismjs/themes/prism-tomorrow.css';
step # 3:
Create the Code component
import React, { useEffect, ReactNode, useState } from "react";
import Prism, { Token } from "prismjs";
export interface CodeProps {
language: 'js' | 'css' | 'json' | 'jsx' | 'typescript' | 'yml' | 'Rust' | 'bash',
children: string
}
function tokenToReactNode(token: Token | string, i: number): ReactNode {
if (typeof token === "string") {
return <span key={i}>{token}</span>
} else if (typeof token.content === "string") {
return (<span key={i} className={`token ${token.type}`}>{token.content}</span>)
} else if (Array.isArray(token.content)) {
return <span key={i} className={`token ${token.type}`}>{token.content.map(tokenToReactNode)}</span>
} else {
return (<span key={i} className={`token ${token.type}`}>{tokenToReactNode(token.content, 0)}</span>)
}
}
export const Code: React.FC<CodeProps> = ({ language, children }) => {
const [data, replaceToken] = useState< Array<string | Token>>([])
useEffect(() => {
import(`prismjs/components/prism-${language}`).then(() => {
const tokens: Array<string | Token> = Prism.languages[ language ]
? Prism.tokenize(children, Prism.languages[ language ])
: [];
replaceToken(tokens)
})
}, [children]);
return (
<pre className={`language-${language}`}>
{data.length ? data.map(tokenToReactNode) : children}
</pre>
);
}
And now the details:
In the first step, we add the prismjs library and types to it in the project.
In the second step, we add a theme. The prismjs library already has several themes and we use one of them. You can
choose which one you like here: node_modules/prismjs/themes. Also, on the library website, you can choose from about a
million more.
In the third step, we create the component itself, this time with comments.
import React, { useEffect, ReactNode, useState } from "react";
import Prism, { Token } from "prismjs";
export interface CodeProps {
language: 'js' | 'css' | 'json' | 'jsx' | 'typescript' | 'yml' | 'Rust' | 'bash',
children: string
}
function tokenToReactNode(token: Token | string, i: number): ReactNode {
if (typeof token === "string") {
return <span key={i}>{token}</span>
} else if (typeof token.content === "string") {
return (<span key={i} className={`token ${token.type}`}>{token.content}</span>)
} else if (Array.isArray(token.content)) {
return <span key={i} className={`token ${token.type}`}>{token.content.map(tokenToReactNode)}</span>
} else {
return (<span key={i} className={`token ${token.type}`}>{tokenToReactNode(token.content, 0)}</span>)
}
}
export const Code: React.FC<CodeProps> = ({ language, children }) => {
const [data, replaceToken] = useState<Array<string | Token>>([])
useEffect(() => {
import(`prismjs/components/prism-${language}`).then(() => {
const tokens: Array<string | Token> = Prism.languages[ language ]
? Prism.tokenize(children, Prism.languages[ language ])
: [];
replaceToken( tokens )
}, [children])
});
return (
<pre className={`language-${language}`}>
{data.length ? data.map(tokenToReactNode) : children}
</pre>
);
}