Typescript + Nextjs + Prismjs or the tale about code highlighting

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 {
  //Prismjs supports a lot more languages. The entire list can be found on the site,
  //this is a list of those languages that will be useful to me.
  language: 'js' | 'css' | 'json' | 'jsx' | 'typescript' | 'yml' | 'Rust' | 'bash',
  children: string
}

//We will use the tokenize method (https://prismjs.com/docs/Prism.html#.tokenize).
//Because other API methods directly manipulate DOM, and that’s not what we want.
//tokenToReactNode is our function that converts the result of executing tokenize into react components.
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 }) => {
  //In the state, we store the code and tokens for the code.
  const [data, replaceToken] = useState<Array<string | Token>>([])
  useEffect(() => {
      //We need to add languages since, by default only markup, CSS, clike, and javascript are available.
      //I did not find a better way, like the one below, if you know - please submit an issue.
      import(`prismjs/components/prism-${language}`).then(() => {
        //If language still not available skip tokenize part
        const tokens: Array<string | Token> = Prism.languages[ language ]
          ? Prism.tokenize(children, Prism.languages[ language ])
          : [];
        //Save the result to the state.
        replaceToken( tokens )
      }, [children])
  });
  //If the array with tokens is empty, print the code from props, otherwise render our beauty.
  return (
    <pre className={`language-${language}`}>
      {data.length ? data.map(tokenToReactNode) : children}
    </pre>
  );
}