【Next.js】Markdown ブログでコードブロックにシンタックスハイライトをかける
2022-12-13 2022-12-08
Markdown とは
Markdown とは一定のルールで書かれたテキスト形式のデータを HTML 形式のデータに変換できる言語です。 Next.js も Markdown から HTML を生成して表示できます。
ルールというのは例えば、以下のように見出しや箇条書きを「#」や「-」で表現します。
# 見出し
本文
- 箇条書き 1
- 箇条書き 2
- 箇条書き 3
↓
普通の Markdown では変換前の素のテキストに HTML タグや JSX を仕込めませんが、 MDX という、 Markdown にそれらを仕込める形式があって、私はそれを使っています。 Next.js で MDX を使うには、next-mdx-remote というプラグインを使うのがおすすめです。
next-mdx-remote
utilities for loading mdx from any remote source as data, rather than as a local import. Latest version: 4.2.0, last published: a month ago. Start using next-mdx-remote in your project by running `npm i next-mdx-remote`. There are 47 other projects in the npm registry using next-mdx-remote.
https://www.npmjs.com/package/next-mdx-remote
(これの使い方については、別記事を書く予定です)
Markdown のコードブロックにシンタックスハイライトをかける
ここからが本題。
技術ブログを見ると、以下のようにプログラムコードのサンプルが強調表示されているのを見かけることがあると思います。
// アラートを表示する
function alert() {
window.alert("アラート");
}
これが今回実装したい シンタックスハイライト です。 これを実装するにはまずは react-syntax-highlighter というプラグインを導入します。
react-syntax-highlighter
syntax highlighting component for react with prismjs or highlightjs ast using inline styles. Latest version: 15.5.0, last published: 9 months ago. Start using react-syntax-highlighter in your project by running `npm i react-syntax-highlighter`. There are 987 other projects in the npm registry using react-syntax-highlighter.
https://www.npmjs.com/package/react-syntax-highlighter
導入方法はとっても簡単。以下の 2 つのコマンドを実行するだけです。
> npm i react-syntax-highlighter
> npm i --save-dev @types/react-syntax-highlighter
次に、Markdown でコードブロックを書いたときに呼び出すコンポーネントを作成します。TypeScript 対応の tsx 形式で書いていきます。
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 を選びました。
react-syntax-highlighter/AVAILABLE_STYLES_PRISM.MD at master · react-syntax-highlighter/react-syntax-highlighter
syntax highlighting component for react with prismjs or highlightjs ast using inline styles - react-syntax-highlighter/AVAILABLE_STYLES_PRISM.MD at master · react-syntax-highlighter/react-syntax-hi...
https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_STYLES_PRISM.MD
最後に code
ですが、こちらはコードブロック内の本文の改行を消すだけです。これも正規表現を使うと簡単に実現します。
これで CodeBlock.tsx
はいったん完成です。
続いて、この CodeBlock.tsx
をコードブロックのコンポーネントとして呼び出します。
gray-matterとnext-mdx-remote のプラグインを使っていますが、他のプラグインを使う場合は、適宜読み替えてください。
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 ファイルを作成します。
```js
// アラートを表示する
function alert() {
window.alert("アラート");
}
```
コードブロックの「js」の部分はハイライトする言語を示しています。以下のリンク先の言語に対応しています。
react-syntax-highlighter/AVAILABLE_LANGUAGES_PRISM.MD at master · react-syntax-highlighter/react-syntax-highlighter
syntax highlighting component for react with prismjs or highlightjs ast using inline styles - react-syntax-highlighter/AVAILABLE_LANGUAGES_PRISM.MD at master · react-syntax-highlighter/react-syntax...
https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_PRISM.MD
うまく読み込まれると、以下のように表示されます。
// アラートを表示する
function alert() {
window.alert("アラート");
}
コードブロックにファイル名を表示する
この調子でコードブロックにファイル名を表示してみましょう。 ファイル名は以下のように言語設定の後に「:」区切りで書くことにします。
```js:public/sample.mdx
// アラートを表示する
function alert() {
window.alert("アラート");
}
```
そして、CodeBlock.tsx
側では、正規表現でファイル名を取得します。
~ 省略 ~
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 を使って以下のように記述できます。
~ 省略 ~
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;
最終的には以下のようになりました。
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;
さらなる改善案
頑張ればインラインコードを装飾したり、コードをクリップボードにコピーするボタンを作ったり、特定の行だけ色を変えたりすることも可能です。
ぜひ研究してみてください。
参考文献
react-markdownでコードをシンタックスハイライトさせる
react-markdownを使用したとき、コードブロックをシンタックスハイライトさせる方法の備忘録です。
https://goodlife.tech/posts/react-markdown-code-highlight/
Syntax Highlight Code in Markdown – Amir Ardalan
Call attention to specific lines of code.
https://amirardalan.com/blog/syntax-highlight-code-in-markdown