react-markdownとreact-syntax-highlighterを利用して、コードブロックをカスタマイズする
Table of contents
- 記事内で使用している主なソフトウェアのバージョン
- 前提条件
- 概要
- 1. react-markdownの準備
- 1-1. react-markdownをインストールする
- 1-2. 記事本文の表示にreact-markdownを利用する
- 2. 言語の指定を行わず、シンタックスハイライトを適用する
- 2-1. react-syntax-highlighterをインストールする
- 2-2. シンタックスハイライトを行う前の状態
- 2-3. 言語の指定を行わず、シンタックスハイライトを適用する
- 3. 言語を指定して、シンタックスハイライトを適用する
- 3-1. コンテンツに言語識別子を追加する
- 3-2. 言語を指定して、シンタックスハイライトを適用する
- 4. ファイル名を表示する
- 4-1. コンテンツにファイル名を追加する
- 4-2. ファイル名を表示する
react-markdown は、マークダウンをレンダリングするReactコンポーネントを提供するライブラリです。マークダウンの変換方法をカスタマイズしたり、特定の要素を指定して、カスタムコンポーネントを適用できます。react-syntax-highlighter は、Reactでシンタックスハイライトを行えるライブラリです。
このチュートリアルでは、react-markdownとreact-syntax-highlighterを利用して、コードブロックの表示をカスタマイズする方法を紹介します。
記事内で使用している主なソフトウェアのバージョン
- Next.js(
next
): 13.5.1 - react-markdown(
react-markdown
): 9.0.0 - react-syntax-highlighter(
react-syntax-highlighter
): 15.5.0
前提条件
- Next.jsのプロジェクトを作成済みであること
Next.jsのセットアップについて知りたい場合は、以下のドキュメントをご確認ください。
- Next.jsの Getting Started のドキュメント
- NewtとNext.jsを利用してブログを作成する
概要
Newtから返却されるマークダウンテキストの内容をもとに、コードブロックを抜き出して、表示をカスタマイズします。
具体的には、以下について説明します。
- 言語の指定を行わず、シンタックスハイライトを適用する
- 言語を指定して、シンタックスハイライトを適用する
- ファイル名を表示する
1. react-markdownの準備
1-1. react-markdownをインストールする
まずは、react-markdown をインストールします。
## npmを利用している場合
npm install react-markdown
## yarnを利用している場合
yarn add react-markdown
1-2. 記事本文の表示にreact-markdownを利用する
もともと以下のような投稿取得メソッドと、投稿詳細ページがあるとします。
article
モデルの body
フィールドにはマークダウンフィールドが定義されており、dangerouslySetInnerHTML
で直接HTMLデータを扱っているものとします。
1import 'server-only'
2import { createClient } from 'newt-client-js'
3import { cache } from 'react'
4import type { Article } from '@/types/article'
5
6const client = createClient({
7 spaceUid: process.env.NEWT_SPACE_UID + '',
8 token: process.env.NEWT_CDN_API_TOKEN + '',
9 apiType: 'cdn',
10})
11
12export const getArticleBySlug = cache(async (slug: string) => {
13 const article = await client.getFirstContent<Article>({
14 appUid: 'blog',
15 modelUid: 'article',
16 query: {
17 slug,
18 select: ['_id', 'title', 'slug', 'body'],
19 },
20 })
21 return article
22})
1import { getArticleBySlug } from '@/lib/newt'
2import styles from '@/app/page.module.css'
3
4type Props = {
5 params: {
6 slug: string
7 }
8}
9
10export default async function Article({ params }: Props) {
11 const { slug } = params
12 const article = await getArticleBySlug(slug)
13 if (!article) return
14
15 return (
16 <main className={styles.main}>
17 <h1>{article.title}</h1>
18 <div dangerouslySetInnerHTML={{ __html: article.body }} />
19 </main>
20 )
21}
ここに、まずreact-markdownを追加します。
react-markdownでは、マークダウンテキストを直接扱うため、body
の返却形式をHTMLではなく、マークダウンテキストにしましょう。
fmt演算子 を利用します。
export const getArticleBySlug = cache(async (slug: string) => {
const article = await client.getFirstContent<Article>({
appUid: 'blog',
modelUid: 'article',
query: {
slug,
select: ['_id', 'title', 'slug', 'body'],
body: {
fmt: 'text',
},
},
})
return article
})
続いて、dangerouslySetInnerHTML
の部分、ReactMarkdown
を利用する書き方に変更しましょう。
import ReactMarkdown from 'react-markdown'
import { getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
(省略)
export default async function Article({ params }: Props) {
const { slug } = params
const article = await getArticleBySlug(slug)
if (!article) return
return (
<main className={styles.main}>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
<div>
<ReactMarkdown>{article.body}</ReactMarkdown>
</div>
</main>
)
}
これでreact-markdownを利用できるようになりました。
2. 言語の指定を行わず、シンタックスハイライトを適用する
次に、言語の指定を行わず、自動でシンタックスハイライトを適用してみましょう。
2-1. react-syntax-highlighterをインストールする
まず react-syntax-highlighter をインストールします。型情報もあわせてインストールしましょう。
## npmを利用している場合
npm install react-syntax-highlighter
npm install -D @types/react-syntax-highlighter
## yarnを利用している場合
yarn add react-syntax-highlighter
yarn add -D @types/react-syntax-highlighter
2-2. シンタックスハイライトを行う前の状態
Newtのマークダウンフィールドでは以下のようにコードブロックを定義しているとします。
```
const hello = () => {
console.log('Hello!')
}
```
すると、返却されるHTMLデータは以下のようになります。
<pre>
<code>
const hello = () => {
console.log('Hello!')
}
</code>
</pre>
pre
タグの子要素として code
タグが入り、その中にコードが記載されます。
※ 詳細は マークダウンエディタ のドキュメントをご確認ください。
この時、フロントエンドでは、以下のようにプレーンな状態で表示されています。
2-3. 言語の指定を行わず、シンタックスハイライトを適用する
ReactMarkdown
では components
オプションを利用して、特定の要素に対してカスタムコンポーネントを割り当てることができます。ここでは pre
要素に対して、カスタムコンポーネントを割り当て、その子供が code
要素である場合に、表示をカスタマイズしましょう。
処理内容の詳細は以下の通りです。
if (childType !== 'code') {
のところで、子要素がcode
であるかチェックしていますconst { children: code } = childProps
でcode
要素内にあるコード情報を取得します- react-syntax-highlighterの
SyntaxHighlighter
コンポーネントがシンタックスハイライトを適用します
import ReactMarkdown from 'react-markdown'
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/default-highlight'
import { getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
import type { ClassAttributes, HTMLAttributes } from 'react'
import type { ExtraProps } from 'react-markdown'
(省略)
export default async function Article({ params }: Props) {
const { slug } = params
const article = await getArticleBySlug(slug)
if (!article) return
const Pre = ({
children,
...props
}: ClassAttributes<HTMLPreElement> &
HTMLAttributes<HTMLPreElement> &
ExtraProps) => {
if (!children || typeof children !== 'object') {
return <code {...props}>{children}</code>
}
const childType = 'type' in children ? children.type : ''
if (childType !== 'code') {
return <code {...props}>{children}</code>
}
const childProps = 'props' in children ? children.props : {}
const { children: code } = childProps
return (
<SyntaxHighlighter>{String(code).replace(/\n$/, '')}</SyntaxHighlighter>
)
}
return (
<main className={styles.main}>
<h1>{article.title}</h1>
<div>
<ReactMarkdown>{article.body}</ReactMarkdown>
<ReactMarkdown
components={{
pre: Pre,
}}
>
{article.body}
</ReactMarkdown>
</div>
</main>
)
}
また、ここでは highlight.js のシンタックスハイライトを適用する SyntaxHighlighter
を利用していますが、Prism
を利用すれば Prism のシンタックスハイライトも適用可能です。
お好きな方をご利用ください。
import SyntaxHighlighter from 'react-syntax-highlighter'
でSyntaxHighlighterをインストールした場合、サーバーコンポーネントではエラーが発生するのでご注意ください。ここでは react-syntax-highlighterのリポジトリにあるコメント を参考に、
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/default-highlight'
からインストールすることで、サーバーコンポーネントでSyntaxHighlighterを利用しています。
また、様々なテーマが用意されており、ファイルをインポートすることで、お好きなスタイルを簡単に適用できます。
ここでは、dracula
というテーマを利用してみましょう。
import ReactMarkdown from 'react-markdown'
import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/default-highlight'
import { dracula } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import { getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
import type { ClassAttributes, HTMLAttributes } from 'react'
import type { ExtraProps } from 'react-markdown'
(省略)
return (
<SyntaxHighlighter>{String(code).replace(/\n$/, '')}</SyntaxHighlighter>
<SyntaxHighlighter style={dracula}>
{String(code).replace(/\n$/, '')}
</SyntaxHighlighter>
)
これで、ハイライトが適用され、以下のように表示されることがわかります。
3. 言語を指定して、シンタックスハイライトを適用する
次に、言語の指定を行うやり方を説明します。
3-1. コンテンツに言語識別子を追加する
まず、Newtのコンテンツに言語識別子を追加しましょう。
マークダウンフィールドの内容を以下のように変更します。
言語識別子として typescript
を入力して、このコードを TypeScript
としてシンタックスハイライトします。
※ 利用可能な言語識別子については react-syntax-highlighterでサポートされている言語(highlight.js) をご確認ください。
```
```typescript
const hello = () => {
console.log('Hello!')
}
```
すると、返却されるHTMLは以下のようになり、code
のクラス名が language-typescript
となることがわかります。このように、言語識別子を記載すると language-{言語識別子}
という名前で、クラスが付与されます。
<pre>
<code class="language-typescript">
const hello = () => {
console.log('Hello!')
}
</code>
</pre>
3-2. 言語を指定して、シンタックスハイライトを適用する
フロントエンドでは、渡されたクラス名をもとに、シンタックスハイライトを行いましょう。
処理内容の詳細は以下の通りです。
childProps
からclassName
を読み取ります- 先頭の
language-
を空文字に変換し、language
として言語識別子を特定します - SyntaxHighlighterの
language
に指定された言語識別子を設定します
(省略)
const Pre = ({
children,
...props
}: ClassAttributes<HTMLPreElement> &
HTMLAttributes<HTMLPreElement> &
ExtraProps) => {
if (!children || typeof children !== 'object') {
return <code {...props}>{children}</code>
}
const childType = 'type' in children ? children.type : ''
if (childType !== 'code') {
return <code {...props}>{children}</code>
}
const childProps = 'props' in children ? children.props : {}
const { children: code } = childProps
const { className, children: code } = childProps
const language = className?.replace('language-', '')
return (
<SyntaxHighlighter style={dracula}>
<SyntaxHighlighter language={language} style={dracula}>
{String(code).replace(/\n$/, '')}
</SyntaxHighlighter>
)
}
(省略)
これで、指定した言語でハイライトが適用されるようになりました。
表示も以下のように変わっていることがわかります。
4. ファイル名を表示する
最後にファイル名も表示してみましょう。
4-1. コンテンツにファイル名を追加する
Newtでは :
を区切り文字として利用することで、言語識別子を正しく認識したまま、返却されるクラス情報に他の情報を追加できます。
```typescript
```typescript:utils/hello.ts
const hello = () => {
console.log('Hello!')
}
```
これで、返却されるHTMLは以下のようになり、code
のクラス名が language-typescript:utils/hello.ts
となることがわかります。
<pre>
<code class="language-typescript:utils/hello.ts">
const hello = () => {
console.log('Hello!')
}
</code>
</pre>
4-2. ファイル名を表示する
渡されたクラス名から、言語識別子とファイル名の情報をそれぞれ取得しましょう。
そして、ファイル名がある場合はヘッダーを追加して表示することにします。
(省略)
const Pre = ({
children,
...props
}: ClassAttributes<HTMLPreElement> &
HTMLAttributes<HTMLPreElement> &
ExtraProps) => {
if (!children || typeof children !== 'object') {
return <code {...props}>{children}</code>
}
const childType = 'type' in children ? children.type : ''
if (childType !== 'code') {
return <code {...props}>{children}</code>
}
const childProps = 'props' in children ? children.props : {}
const { className, children: code } = childProps
const language = className?.replace('language-', '')
const classList = className ? className.split(':') : []
const language = classList[0]?.replace('language-', '')
const fileName = classList[1]
return (
<SyntaxHighlighter language={language} style={dracula}>
{String(code).replace(/\n$/, '')}
</SyntaxHighlighter>
<>
{fileName && (
<div>
<span>{fileName}</span>
</div>
)}
<SyntaxHighlighter language={language} style={dracula}>
{String(code).replace(/\n$/, '')}
</SyntaxHighlighter>
</>
)
}
(省略)
これで、pre
要素の前に、ファイル名の情報を持った div
要素が追加されるようになりました。
あとは、お好みでスタイルを当てていただければ、以下のようにファイル名を表示できます。