React でリアルタイム Markdown エディターを作る【シンタックスハイライト対応】


フロントエンドエンジニアの茶木です。Markdown 便利ですよね。
先日、React 上で Markdown をHTMLとして表示するケースがありました。
react-markdown というライブラリを使って簡単に実装ができました。簡単すぎてブログの記事とするには少し物足りないので、シンタックスハイライトの追加といったアドバンスも含めて解説していきます。

基本

react-markdown の基本は非常に簡単です。children にパースしたい Markdown の文字列を読み込ませればそれだけで変換されます。

import ReactMarkdown from "react-markdown";

const text = "テキスト **太字** *斜体* (Markdown)[https://github.com/remarkjs/react-markdown]";
const MarkdownStatic = () => {
  return (
   <ReactMarkdown>{text}</ReactMarkdown>
  );
};

たとえば、このコードなら、テキスト 太字 斜体 Markdown のようにパースされます。

リアルタイムエディタ

踏まえて、左のテキストエリアに、Markdown 形式で入力すると、リアルタイムに右のプレビュー画面に HTMLにパースされて出力されるようなものを作っていきます。

import { useState } from "react";
import ReactMarkdown from "react-markdown";
import styles from "./Markdown.module.css";

const MarkdownEditor = () => {
  const [text, setText] = useState("");

  return (
    <div className={styles.container}>
      <textarea
        value={text}
        onChange={(e) => setText(e.target.value)}
        className={styles.editor}
      />
      <div className={styles.preview}>
        <ReactMarkdown>{text}</ReactMarkdown>
      </div>
    </div>
  );
};

useState で管理して textarea の更新をトリガーにパースし直されるようにしました。これだけで入力に即応してプレビューが更新されるエディタは完成です。

GFM 拡張

GFM (GitHub Flavored Markdown) というMarkdown の拡張仕様に対応します。
これは、GitHub で使用される拡張仕様で、打ち消し線、チェックボックス、テーブル、自動リンクなどがあります。

import remarkGfm from "remark-gfm";

<ReactMarkdown remarkPlugins={[remarkGfm]}>{text}</ReactMarkdown>

remarkGfm は、Remark という Markdown パーサーのプラグインです。これを remarkPlugins に渡すだけです。

SyntaxHighlighter

次のように構文のハイライトも対応します。対応している言語はいくつもあり、```typescript といった形でコードブロックに言語を指定できます。対応する言語があればパースして着色してくれます。

import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";

<div className={styles.preview}>
  <ReactMarkdown
    remarkPlugins={[remarkGfm]}
    components={{
      code(props) {
        const { children, className } = props;
        const match = /language-(\w+)/.exec(className || "");
        return match ? (
          <SyntaxHighlighter language={match[1]}>
            {children}
          </SyntaxHighlighter>
        ) : (
          <code className={className}>{children}</code>
        );
      },
    }}
  >
    {text}
  </ReactMarkdown>
</div>

コードの解説です。react-syntax-highlighter をインポートしておきます。
ReactMarkdown の components 内で code() とすることで、パース後に <code></code> となるものをフックします。<code></code> となるのは、前述の ``` で始まるコードブロックと ` で始まるインラインのコードです。

className には、コードブロックに指定した言語名が、language-xxx の形で格納されるので、言語名を取得します。取得できれば、SyntaxHighlighterlanguage として children といっしょに渡せばハイライトしてくれます。言語名が取得できないときは、言語指定なしのコードブロックもしくは、インラインのコードとなり、通常と同様の処理を行っています。

さらなる拡張と改良

SyntaxHighlighter で components よりフックを行っています。たとえば a タグをフックして、Amazon や Youtube のリンクと判断すれば埋め込みを行うといったことも拡張が可能です。

他にも使い勝手に関する改良として、プログラミングコードを扱う場合ではタブキーによるインデントの操作ができると便利ですが、現状ではタブキーはフォーカスの移動となり行えません。もうひとつは、テキストエリアでスクロールを行ったとき、プレビューのスクロール位置も連動すると便利そうです。

これらは次回以降のテーマとしておきます。
Gaji-Labo は支援しているスタートアップに対して、ただの作業者ではなく頼れるパートナーとして支援できる仕事のやり方を大切にしています。便利なライブラリを使う場合であっても、改良点やケアすべき点は多いものです。プラスワンの価値を考えていけるようなエンジニアでありたいと思います。

Gaji-Labo フロントエンドエンジニア向けご案内資料

タグ


投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。