🐷

React Syntax Highlighterで行ごとにハイライト表示する

2024/09/28に公開

React アプリ上ででソースコードを色つき表示するためのライブラリとしては React Syntax Highlighter が最も有名で、かつ導入も非常に簡単です。しかし、React Syntax Highlighter が提供する機能だけでは実現できないこともあり、その中でもよくある要望は行ごとのハイライトだと思います。本記事では React Syntax Highlighter で行ごとのハイライトを行う手順を紹介します。

https://youtu.be/G1eb5PXZadE

手順の解説

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 に渡す languagestyle は公式レポジトリの説明通りです。

<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が「どの行をハイライトするか?」という判断を行っている箇所になります。linePropsreturnでは「行ごとの<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 {};
  }
}}
GitHubで編集を提案

Discussion