cheerioとhighlight.jsを利用して、コードブロックをカスタマイズする
Table of contents
cheerio は、Node.js上でjQueryライクなAPIでHTMLをパース・操作できるJavaScriptライブラリです。highlight.js は、Node.js上でコードのシンタックスハイライトを行えるライブラリです。
このチュートリアルでは、cheerioとhighlight.jsを利用して、コードブロックの表示をカスタマイズする方法を紹介します。
記事内で使用している主なソフトウェアのバージョン
- Next.js(
next
): 13.5.1 - cheerio(
cheerio
): 1.0.0-rc.12 - highlight.js(
highlight.js
): 11.9.0
前提条件
- Next.jsのプロジェクトを作成済みであること
Next.jsのセットアップについて知りたい場合は、以下のドキュメントをご確認ください。
- Next.jsの Getting Started のドキュメント
- NewtとNext.jsを利用してブログを作成する
概要
Newtから返却されるHTMLデータ(マークダウンフィールド)の内容をもとに、コードブロックの表示をカスタマイズします。
具体的には、以下について説明します。
- 言語の指定を行わず、シンタックスハイライトを適用する
- 言語を指定して、シンタックスハイライトを適用する
- ファイル名を表示する
1. cheerio・highlight.jsをインストールする
まずは、cheerio と highlight.js をインストールします。
## npmを利用している場合
npm install cheerio highlight.js
## yarnを利用している場合
yarn add cheerio highlight.js
2. 言語の指定を行わず、シンタックスハイライトを適用する
まず、言語の指定を行わず、自動でシンタックスハイライトを適用してみましょう。
2-1. シンタックスハイライトを行う前の状態
もともと以下のような投稿詳細ページがあるとします。
article
モデルの body
フィールドにはマークダウンフィールドが定義されており、dangerouslySetInnerHTML
で直接HTMLデータを扱っているものとします。
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}
また、Newtのマークダウンフィールドでは以下のようにコードブロックを定義しているとします。
```
const hello = () => {
console.log('Hello!')
}
```
この時、フロントエンドの表示は以下のようになります。
2-2. 言語の指定を行わず、シンタックスハイライトを適用する
ここにシンタックスハイライトを追加します。
まず、マークダウンフィールドから返却されるHTMLデータの形式をおさらいしましょう。コードブロックの場合、以下のような形式で返されます。
※ 詳細は マークダウンエディタ のドキュメントをご確認ください。
<pre>
<code>
const hello = () => {
console.log('Hello!')
}
</code>
</pre>
pre
タグの子要素として code
タグが入り、その中にコードが記載されます。
cheerioで要素を指定する場合、$('pre code')
とすることで、コードブロックの情報を抽出できます。
処理内容の詳細は以下の通りです。
- cheerioの load 関数に
article.body
を渡すことでHTMLのパースを行います $('pre code').each((_, elm) => { /* 変換処理 */ })
として、コードブロック要素のみを抽出して順次処理を実行しますhljs.highlightAuto($(elm).text())
として、抽出した要素にシンタックスハイライトを適用します- スタイルで必要となる
hljs
クラスを、code要素に付与します $.html()
を実行して、パースしたDOMノードを再びHTMLへと変換してarticle.body
に再代入します
import { load } from 'cheerio'
import hljs from 'highlight.js'
import { getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
type Props = {
params: {
slug: string
}
}
export default async function Article({ params }: Props) {
const { slug } = params
const article = await getArticleBySlug(slug)
if (!article) return
const $ = load(article.body)
$('pre code').each((_, elm) => {
const result = hljs.highlightAuto($(elm).text())
$(elm).html(result.value)
$(elm).addClass('hljs')
})
article.body = $.html()
return (
<main className={styles.main}>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</main>
)
}
また、highlight.jsでは様々なテーマが用意されており、cssファイルをインポートすることで、お好きなスタイルを簡単に適用できます。
highlight.jsの Demo を見ると、すべてのテーマを確認することができます。
ここでは、github-dark.css
というテーマを利用してみましょう。
import { load } from 'cheerio'
import hljs from 'highlight.js'
import { getArticleBySlug } from '@/lib/newt'
import styles from '@/app/page.module.css'
import 'highlight.js/styles/github-dark.css'
(省略)
これで、ハイライトが適用され、以下のように表示されることがわかります。
3. 言語を指定して、シンタックスハイライトを適用する
次に、言語の指定を行うやり方を説明します。
3-1. コンテンツに言語識別子を追加する
まず、Newtのコンテンツに言語識別子を追加しましょう。
マークダウンフィールドの内容を以下のように変更します。
言語識別子として ts
を入力して、このコードを TypeScript
としてシンタックスハイライトします。
※ 利用可能な言語識別子については highlight.jsでサポートされている言語 をご確認ください。
```
```ts
const hello = () => {
console.log('Hello!')
}
```
すると、返却されるHTMLは以下のようになり、code
のクラス名が language-ts
となることがわかります。このように、言語識別子を記載すると language-{言語識別子}
という名前で、クラスが付与されます。
<pre>
<code class="language-ts">
const hello = () => {
console.log('Hello!')
}
</code>
</pre>
3-2. 言語を指定して、シンタックスハイライトを適用する
フロントエンドでは、渡されたクラス名をもとに、シンタックスハイライトを行いましょう。
処理内容の詳細は以下の通りです。
$(elm).attr('class')
でcode
要素に付与されているクラスを読み取ります- 先頭の
language-
を空文字に変換し、language
として言語識別子を特定します hljs.highlight($(elm).text(), { language })
で指定された言語識別子でハイライトを行います- もし、言語が指定されていなかったり、ハイライト時にエラーが発生した場合は
highlightAuto
を使います
(省略)
export default async function Article({ params }: Props) {
const { slug } = params
const article = await getArticleBySlug(slug)
if (!article) return
const $ = load(article.body)
$('pre code').each((_, elm) => {
const result = hljs.highlightAuto($(elm).text())
const className = $(elm).attr('class')
const language = className?.replace('language-', '')
let result
if (language) {
try {
result = hljs.highlight($(elm).text(), { language })
} catch {
result = hljs.highlightAuto($(elm).text())
}
} else {
result = hljs.highlightAuto($(elm).text())
}
$(elm).html(result.value)
$(elm).addClass('hljs')
})
article.body = $.html()
return (
<main className={styles.main}>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</main>
)
}
これで、指定した言語でハイライトが適用されるようになりました。
表示も以下のように変わっていることがわかります。
4. ファイル名を表示する
最後にファイル名も表示してみましょう。
4-1. コンテンツにファイル名を追加する
Newtでは :
を区切り文字として利用することで、言語識別子を正しく認識したまま、返却されるクラス情報に他の情報を追加できます。
```ts
```ts:utils/hello.ts
const hello = () => {
console.log('Hello!')
}
```
これで、返却されるHTMLは以下のようになり、code
のクラス名が language-ts:utils/hello.ts
となることがわかります。
<pre>
<code class="language-ts:utils/hello.ts">
const hello = () => {
console.log('Hello!')
}
</code>
</pre>
4-2. ファイル名を表示する
渡されたクラス名から、言語識別子とファイル名の情報をそれぞれ取得しましょう。
そして、ファイル名がある場合はヘッダーを追加して表示することにします。
(省略)
export default async function Article({ params }: Props) {
const { slug } = params
const article = await getArticleBySlug(slug)
if (!article) return
const $ = load(article.body)
$('pre code').each((_, elm) => {
const className = $(elm).attr('class')
const language = className?.replace('language-', '')
const classList = className ? className.split(':') : []
const language = classList[0]?.replace('language-', '')
const fileName = classList[1]
let result
if (language) {
try {
result = hljs.highlight($(elm).text(), { language })
} catch {
result = hljs.highlightAuto($(elm).text())
}
} else {
result = hljs.highlightAuto($(elm).text())
}
$(elm).html(result.value)
$(elm).addClass('hljs')
if (fileName) {
$(elm).parent().before(`<div><span>${fileName}</span></div>`)
}
})
article.body = $.html()
return (
<main className={styles.main}>
<h1>{article.title}</h1>
<div dangerouslySetInnerHTML={{ __html: article.body }} />
</main>
)
}
これで、pre
要素の前に、ファイル名の情報を持った div
要素が追加されるようになりました。
あとは、お好みでスタイルを当てていただければ、以下のようにファイル名を表示できます。