YuTa Extend

オリジナル楽曲とIT系個人開発紹介のブログ

【Next.js】Markdown ブログでコードブロックにシンタックスハイライトをかける

2022-12-13  2022-12-08

【Next.js】Markdown ブログでコードブロックにシンタックスハイライトをかける

Markdown とは

Markdown とは一定のルールで書かれたテキスト形式のデータを HTML 形式のデータに変換できる言語です。 Next.js も Markdown から HTML を生成して表示できます。

ルールというのは例えば、以下のように見出しや箇条書きを「#」や「-」で表現します。

Copied!!
# 見出し

本文

- 箇条書き 1
- 箇条書き 2
- 箇条書き 3

markdown

普通の Markdown では変換前の素のテキストに HTML タグや JSX を仕込めませんが、 MDX という、 Markdown にそれらを仕込める形式があって、私はそれを使っています。 Next.js で MDX を使うには、next-mdx-remote というプラグインを使うのがおすすめです。

(これの使い方については、別記事を書く予定です)

Markdown のコードブロックにシンタックスハイライトをかける

ここからが本題。

技術ブログを見ると、以下のようにプログラムコードのサンプルが強調表示されているのを見かけることがあると思います。

Copied!!
// アラートを表示する
function alert() {
  window.alert("アラート");
}

これが今回実装したい シンタックスハイライト です。 これを実装するにはまずは react-syntax-highlighter というプラグインを導入します。

導入方法はとっても簡単。以下の 2 つのコマンドを実行するだけです。

Copied!!
> npm i react-syntax-highlighter
> npm i --save-dev @types/react-syntax-highlighter

次に、Markdown でコードブロックを書いたときに呼び出すコンポーネントを作成します。TypeScript 対応の tsx 形式で書いていきます。

CodeBlock.tsx
Copied!!
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";

type Props = {
  className?: string;
  children?: React.ReactNode;
};
const CodeBlock: React.FC<Props> = ({ className, children = "" }: Props) => {
  const match = /language-(\w+)/.exec(className || "");
  const language = match && match[1] ? match[1] : "";
  const code = String(children).replace(/\n$/, "");
  return (
    <>
      <div>
        <SyntaxHighlighter language={language} style={atomDark}>
          {code}
        </SyntaxHighlighter>
      </div>
    </>
  );
};

export default CodeBlock;

<SyntaxHighlighter>language には、CodeBlock.tsx に渡された className の値を加工して設定します。 className の値は 「language-(言語)」 の形式なので、正規表現を使って言語の部分だけ切り出して language に設定します。

また、<SyntaxHighlighter>style は、以下のリストからお好みで選んで設定します。 私は atomDark を選びました。

最後に code ですが、こちらはコードブロック内の本文の改行を消すだけです。これも正規表現を使うと簡単に実現します。

これで CodeBlock.tsx はいったん完成です。

続いて、この CodeBlock.tsx をコードブロックのコンポーネントとして呼び出します。 gray-matternext-mdx-remote のプラグインを使っていますが、他のプラグインを使う場合は、適宜読み替えてください。

PostPage.tsx
Copied!!
import type { InferGetStaticPropsType, NextPage } from "next";
import { ReactNode } from "react";
import path from "path";
import fs from "fs";
import matter from "gray-matter";
import { serialize } from "next-mdx-remote/serialize";
import { MDXRemote } from "next-mdx-remote";
import CodeBlock from "CodeBlock";

export async function getStaticProps() {
  const postFilePath = path.join(process.cwd(), "public/sample.mdx");
  const source = fs.readFileSync(postFilePath);
  const { content, data } = matter(source);
  const mdxSource = await serialize(content, {
    mdxOptions: {
      remarkPlugins: [],
      rehypePlugins: [],
    },
    scope: data,
  });

  return {
    props: {
      mdxSource: mdxSource,
    },
  };
}

const components = {
  code: (props: JSX.IntrinsicAttributes & { children?: ReactNode }) => (
    <CodeBlock {...props} />
  ),
};

type Props = InferGetStaticPropsType<typeof getStaticProps>;
const PostPage: NextPage<Props> = (props: Props) => {
  return <MDXRemote {...props.mdxSource} components={components} />;
};

export default PostPage;

最後に、読み込む MDX ファイルを作成します。

public/sample.mdx
Copied!!
```js
// アラートを表示する
function alert() {
  window.alert("アラート");
}
```

コードブロックの「js」の部分はハイライトする言語を示しています。以下のリンク先の言語に対応しています。

うまく読み込まれると、以下のように表示されます。

Copied!!
// アラートを表示する
function alert() {
  window.alert("アラート");
}

コードブロックにファイル名を表示する

この調子でコードブロックにファイル名を表示してみましょう。 ファイル名は以下のように言語設定の後に「:」区切りで書くことにします。

public/sample.mdx
Copied!!
```js:public/sample.mdx
// アラートを表示する
function alert() {
  window.alert("アラート");
}
```

そして、CodeBlock.tsx 側では、正規表現でファイル名を取得します。

CodeBlock.tsx
Copied!!
  ~ 省略 ~
  const match = /language-(\w+)(:?.*)/.exec(className || "");
  const language = match && match[1] ? match[1] : "";
  const fileName = match && match[2] ? match[2].slice(1) : "";
  const code = String(children).replace(/\n$/, "");
  ~ 省略 ~

次にファイル名の配置ですが、<SyntaxHighlighter> タグのラッパーを用意して、 その中に配置すれば OK です。

ただし、本ブログのように CSS で装飾したい場合は <SyntaxHighlighter> タグにもデフォルトで CSS が効いているので、 これを上書きする必要があります。

これは styled-jsx を使って以下のように記述できます。

CodeBlock.tsx
Copied!!
  ~ 省略 ~
  const syntaxHighlighterClass = fileName
    ? "code-block-with-title"
    : "code-block";
  return (
    <>
      <div className="code-block-wrapper">
        {fileName && <div className="code-block-title">{fileName}</div>}
        <SyntaxHighlighter
          language={language}
          style={atomDark}
          className={syntaxHighlighterClass}
        >
          {code}
        </SyntaxHighlighter>
      </div>
      <style jsx>{`
        .code-block-wrapper {
          font-size: 0.9rem;
          margin-bottom: 2rem;
        }
        .code-block-title {
          display: inline-block;
          border-radius: 0.3rem 0.3rem 0 0;
          background-color: #323e52;
          padding: 0.55rem 1rem;
          color: white;
          font-size: 0.8rem;
          font-family: Inconsolata, Monaco, Consolas, "Courier New", Courier,
            monospace;
        }
      `}</style>
      <style jsx global>{`
        .code-block {
          border-radius: 0.3rem !important;
          padding: 1.5rem !important;
        }
        .code-block-with-title {
          border-radius: 0 0.3rem 0.3rem 0.3rem !important;
          padding: 1.5rem !important;
          margin-top: 0 !important;
        }
      `}</style>
    </>
  );
};

export default CodeBlock;

最終的には以下のようになりました。

CodeBlock.tsx
Copied!!
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomDark } from "react-syntax-highlighter/dist/cjs/styles/prism";

type Props = {
  className?: string;
  children?: React.ReactNode;
};
const CodeBlock: React.FC<Props> = ({ className, children = "" }: Props) => {
  const match = /language-(\w+)(:?.*)/.exec(className || "");
  const language = match && match[1] ? match[1] : "";
  const fileName = match && match[2] ? match[2].slice(1) : "";
  const code = String(children).replace(/\n$/, "");
  const syntaxHighlighterClass = fileName
    ? "code-block-with-title"
    : "code-block";
  return (
    <>
      <div className="code-block-wrapper">
        {fileName && <div className="code-block-title">{fileName}</div>}
        <SyntaxHighlighter
          language={language}
          style={atomDark}
          className={syntaxHighlighterClass}
        >
          {code}
        </SyntaxHighlighter>
      </div>
      <style jsx>{`
        .code-block-wrapper {
          font-size: 0.9rem;
          margin-bottom: 2rem;
        }
        .code-block-title {
          display: inline-block;
          border-radius: 0.3rem 0.3rem 0 0;
          background-color: #323e52;
          padding: 0.55rem 1rem;
          color: white;
          font-size: 0.8rem;
          font-family: Inconsolata, Monaco, Consolas, "Courier New", Courier,
            monospace;
        }
      `}</style>
      <style jsx global>{`
        .code-block {
          border-radius: 0.3rem !important;
          padding: 1.5rem !important;
        }
        .code-block-with-title {
          border-radius: 0 0.3rem 0.3rem 0.3rem !important;
          padding: 1.5rem !important;
          margin-top: 0 !important;
        }
      `}</style>
    </>
  );
};

export default CodeBlock;

さらなる改善案

頑張ればインラインコードを装飾したり、コードをクリップボードにコピーするボタンを作ったり、特定の行だけ色を変えたりすることも可能です。

ぜひ研究してみてください。

参考文献

関連記事