概要
GatsbyJS での汎用的な構文ハイライト(syntax highlight)の実装方法を紹介します。
本ブログは Markdown ファイルと WordPress の両方のコンテンツを Gatsby で表示していて、その場合に使えるやり方です。もちろんどちらかのみの場合でも OK です。
react-syntax-highlighter
というプラグインを使って、それに上手くコンテンツデータを渡すということをやってます。
参考にしたサイトはこちら。
https://dimitri.codes/adding-syntax-highlighting-wordpress-gatsby/
また、実コードが見たい方は、実際の変更コミットはこちらを見てください。
https://github.com/nisioka/sun0range.com/commit/7a469363fc9b309f720cf7903ba1da5ce39f9895
前提・準備
Gatsby でのコンテンツを表示に、dangerouslySetInnerHtml={{__html=content}}
を使用していることを想定していて、それを書き換えていきます。
また、TypeScript も使用しているので以降のコードも.ts もしくは.tsx です。
まずは依存関係のインストールを行ってください。(TypeScript なので、開発のしやすさのために@types も dev に入れてます。)
1npm install --save html-react-parser react-syntax-highlighter
2npm install --save-dev @types/react-syntax-highlighter
3
実装
まず、修正前の実装イメージを示します。post.contentがコンテンツデータで、それを dangerouslySetInnerHTML に渡して表示しているとします。
1import * as React from "react"
2
3const BlogPostTemplate = () => {
4 // ノイズになるため省略。ここでpostのデータを取得したりしている。
5
6 return (
7 <>
8 // ノイズになるため省略。実際は他にもレイアウトしている。
9 <section dangerouslySetInnerHTML={{ __html: post.content }} />
10 </>
11 )
12}
13
修正後のコードが下記です。
1import * as React from "react"
2import { Link, graphql } from "gatsby"
3
4import parse, { domToReact } from "html-react-parser"
5import SyntaxHighlighter from "react-syntax-highlighter"
6import { androidstudio } from "react-syntax-highlighter/dist/cjs/styles/hljs"
7
8const BlogPostTemplate = () => {
9 // ノイズになるため省略。ここでpostのデータを取得したりしている。
10
11 return (
12 <>
13 // ノイズになるため省略。実際は他にもレイアウトしている。
14 <section>{parse(post.content, { replace: replaceCode })}</section>
15 </>
16 )
17}
18
19const replaceCode = (node: any) => {
20 if (!node) return node
21 if (node.name === "pre") {
22 const dom = domToReact(getCode(node))
23 let result = ""
24 switch (typeof dom) {
25 case "string":
26 result = dom as string
27 break
28 case "object":
29 if (Array.isArray(dom)) {
30 // React.JSX.Element[]
31 const elmArr = dom as React.JSX.Element[]
32 elmArr.map(elm => {
33 if (elm.props && elm.props.children) {
34 result += elm.props.children as string
35 }
36 })
37 } else {
38 // React.JSX.Element
39 const elm = dom as React.JSX.Element
40 if (elm.props && elm.props.children) {
41 result = elm.props.children as string
42 }
43 }
44 break
45 }
46
47 return (
48 node.children.length > 0 && (
49 <SyntaxHighlighter
50 style={androidstudio}
51 language={getLanguage(node)}
52 showLineNumbers={true}
53 >
54 {result}
55 </SyntaxHighlighter>
56 )
57 )
58 }
59}
60
61const getLanguage = (node: any) => {
62 function getClassInLanguage(className: string) {
63 let result = ""
64 className.split(/\s+/).forEach(s => {
65 if (s.startsWith("language-")) {
66 result = s.replace("language-", "")
67 break
68 }
69 })
70 return result
71 }
72
73 if (node.attribs.class && node.attribs.class !== "wp-block-code") {
74 return getClassInLanguage(node.attribs.class as string)
75 } else if (node.children[0]?.attribs?.class) {
76 return getClassInLanguage(node.children[0].attribs.class as string)
77 }
78 return "java" // default
79}
80
81const getCode = (node: any) => {
82 if (node.children.length > 0 && node.children[0].name === "code") {
83 return node.children[0].children
84 } else {
85 return node.children
86 }
87}
88
上記コードについて説明します。
- 元々
dangerouslySetInnerHTML
を使っていたものを、html-react-parser
ライブラリのparse
関数を使うように変更します。 parse
関数は HTML を React コンポーネントに変換し、後述する自作のreplaceCode
関数を使用して<code>
ブロックを検出します。それ以外はそのままです。replaceCode
関数はコードブロックをハイライト表示するための処理を行います。parse
関数が DOM 要素を node として一つづつ渡してくるので、node が「<pre>
タグ内に<code>
の子を持つ要素」以外の要素の場合は、置き換えせずそのままです。- 「
<pre>
タグ内に<code>
の子を持つ要素」である場合、その中のコードを取得し、react-syntax-highlighter
ライブラリのSyntaxHighlighter
コンポーネントで整形します。 SyntaxHighlighter
コンポーネントでは、言語やスタイルが選べて、
※1: WordPress の「高度な設定」の例。
終わりに
以上で、Gatsby アプリケーションを実行し、構文ハイライトされたコードを確認できるはずです。本記事内のコードも構文ハイライトされて見えているはずです。 これで、WordPress と Gatsby で構文ハイライトを実装しました。完全な例に興味がある場合は、このブログのソースコードを 以下の GitHub で確認できます。 https://github.com/nisioka/sun0range.com/blob/master/src/templates/blog-post.tsx