React Syntax Highlighterで行ごとにハイライト表示する
React アプリ上ででソースコードを色つき表示するためのライブラリとしては React Syntax Highlighter が最も有名で、かつ導入も非常に簡単です。しかし、React Syntax Highlighter が提供する機能だけでは実現できないこともあり、その中でもよくある要望は行ごとのハイライトだと思います。本記事では React Syntax Highlighter で行ごとのハイライトを行う手順を紹介します。
手順の解説
React Syntax Highlighter を使うには、公式の GitHub レポジトリにもあるように、npm install を実行します。本記事では TypeScript 用に2行目にある@types
のインストールも行っています。
npm install react-syntax-highlighter --save
npm install @types/react-syntax-highlighter --save-dev
あとはこのような React コンポーネントを書けば、行ごとのハイライトができます。
import SyntaxHighlighter from "react-syntax-highlighter";
import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs";
import styles from "./Component.module.css";
interface Props {
codeString: string;
}
export const Component = (props: Props) => {
const highlights = [
// 2行から3行をハイライト
{ start: 2, end: 3 },
// 6行から8行をハイライト
{ start: 6, end: 8 },
];
return (
<SyntaxHighlighter
language="javascript"
style={docco}
// CSS Modules - Next.jsではデフォルトでサポート、CSSの中身は次の通り
// .component > code > span {
// display: block;
// }
className={styles.component}
// ソースコード各行を<span>で囲む
wrapLines
// 直下の lineProps で (lineNumber: number) を引数として使うために必要
showLineNumbers
lineProps={(lineNumber: number) => {
const hFound = highlights.find(
(h) => h.start <= lineNumber && lineNumber <= h.end
);
if (hFound) {
return {
"data-highlight-start": hFound.start,
"data-highlight-end": hFound.end,
// lineProps内ではclassNameは使えないのでstyleを使う必要がある
style: { backgroundColor: "yellow" },
};
} else {
return {};
}
}}
>
{props.codeString}
</SyntaxHighlighter>
);
};
ソースコードの解説
CSS セレクタによる行ごとの<span>の display:block 化
以下の部分で CSS Modules を使っていますが、Next.js で React を利用するのであれば、CSS Modules は最初からサポートされています。
import styles from "./Component.module.css";
...
className={styles.component}
CSS Modules を使っていなくても、SaSS でも Emotion でも、必要な CSS は以下と同等のものです。
.component > code > span {
display: block;
}
display: block
によって行ごとの<span>
タグは横幅いっぱいの広がりを持ち、行ハイライトが行の途中で切れることなく自然になります。冒頭の 45 秒程度の動画を見ていただければわかりやすいと思います。
ハイライト行番号の指定
ハイライト行番号の指定は、以下のような object を作成しておくとわかりやすいでしょう。ローカル変数ではなく、React の props として渡す方が実用的ですので、実際に使う場合はそのように書き換えてください。
const highlights = [
// 2行から3行をハイライト
{ start: 2, end: 3 },
// 6行から8行をハイライト
{ start: 6, end: 8 },
];
上記の指定は以下のような表示になります。
SyntaxHighlighter コンポーネントの呼び出しと指定する props
SyntaxHighlighter に渡す language
と style
は公式レポジトリの説明通りです。
<SyntaxHighlighter
language="javascript"
style={docco}
...
className={styles.component}
...
>
className={styles.component}
部分はすでに説明したので省略します。
wrapLines
の指定は必須です。似た名前の wrapLingLines
との違いが紛らわしいのですが、wrapLines
はソースコード各行を<span>
で囲むためのもので、行ごとのハイライトにはなくてはならないものです。一方のwrapLongLines
は長すぎるソースコード行の折り返しを行うものです。
// ソースコード各行を<span>で囲む
wrapLines;
showLineNumbers
は次に指定するlineProps
との組み合わせで必要になります。不思議なことに React Syntax Highlighter はshowLineNumbers
を指定しないとlineProps
のコールバック関数に渡される引数がnull
になってしまうので、lineProps
を使いたければshowLineNumbers
を指定しなくてはなりません。
// 直下の lineProps で (lineNumber: number) を引数として使うために必要
showLineNumbers;
以下のlineProps
が「どの行をハイライトするか?」という判断を行っている箇所になります。lineProps
のreturn
では「行ごとの<span>
タグ」に追加する attributes を指定するのですが、残念なことにclassName
は使えません。これは React Syntax Highlighter が内部的に「lineProps
で指定したclassName
をキャンセルする」という動作をしてしまうためです。
lineProps={(lineNumber: number) => {
const hFound = highlights.find(
(h) => h.start <= lineNumber && lineNumber <= h.end
);
if (hFound) {
return {
// lineProps内ではclassNameは使えないのでstyleを使う必要がある
style: { backgroundColor: "yellow" },
};
} else {
return {};
}
}}
Discussion