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