📖

Reactでマークダウンエディタを作ってみよう

2021/08/26に公開

はじめに

今回はreact-simplemde-editorというライブラリを使用してマークダウンエディタを作成します。react-simplemde-editorとはEasyMDEをラップしたReactコンポーネントです。
詳しい説明や使い方はGitHubにあります。
ライブラリのおかげで簡単に実装することができます!

環境

  • React
  • react-simplemde-editor(EasyMDE)
  • marked
  • highit.js
  • Cloud Storage for Firebase (画像の保存機能を作る場合は必要)

マークダウンエディタの実装

基本的なエディタの実装

実装に必要なパッケージの簡単な説明をします

  • marked(GitHub
    これはマークダウン形式の文字列をHTMLに変換してくれます

  • DOMPurify(GitHub
    これはHTMLをサニタイズし、XSS攻撃を防いでくれます。

  • highlight.js(GitHub
    これはコードを挿入している部分にハイライトをつけてくれます。

必要なパッケージをインストールします。

npm install --save react-simplemde-editor easymde marked highlight.js dompurify

そしてマークダウンエディタを実装していきます。
コードは下のようになり、とても簡単です。

import React, { useState } from "react";
import SimpleMde from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";

export const MarkdownEditor = () => {
  const [markdownValue, setMarkdownValue] = useState("Initial value");

  const onChange = (value) => {
    setMarkdownValue(value);
  };

  return <SimpleMde value={markdownValue} onChange={onChange} />;
};

export default MarkdownEditor;

エディタの内容をstateで管理して、エディタが変更(文字が入力・削除など)されるたびにonChange関数が呼び出され内容が更新されるという仕組みです。
これで下のようなエディタが完成しました。

プレビュー機能の追加

マークダウン形式の文字列をmarkedでHTMLに変換して表示してみましょう。
コードは下のようになります。

const MarkdownEditor = () => {
 const [markdownValue, setMarkdownValue] = useState("Initial value");

 const onChange = (value) => {
   setMarkdownValue(value);
 };

 return 
 <>
    <SimpleMde value={markdownValue} onChange={onChange} />
    <div>
       <div dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(marked(markdownValue))}}></div>
    </div>
 </>;  
};

export default MarkdownEditor;

Stateとして管理しているマークダウン形式の文字列をHTMLに変換する際にはDOMPurifyによってHTMLをサニタイズ(エスケープ)しています。

これで下のようなプレビュー付きエディタの完成です。

ハイライトをつけよう

さらにハイライトもつけてみましょう。ここでhighlight.jsを使います。
ハイライトのテーマは"highlight.js/styles/github.css"を使います。

コードは次のようになります。

import React, { useState } from "react";
import SimpleMde from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";
import marked from "marked";
import DOMPurify from "dompurify";
import highlightjs from "highlight.js";
import "highlight.js/styles/github.css";

const MarkdownEditor = () => {
   // ハイライトの設定
 marked.setOptions({
   highlight: (code, lang) => {
     return highlightjs.highlightAuto(code, [lang]).value;
   },
 });

 const [markdownValue, setMarkdownValue] = useState("");

 const onChange = (value) => {
   setMarkdownValue(value);
 };

 return (
   <div>
     <SimpleMde value={markdownValue} onChange={onChange} />
     <div
       dangerouslySetInnerHTML={{
         __html: DOMPurify.sanitize(marked(markdownValue)),
       }}
     ></div>
   </div>
 );
};

export default MarkdownEditor;

これでコードの部分をハイライトしてくれるようになりました。

あとはエディタとプレビューを横並びにするなどレイアウトは自分でカスタマイズしてみてください。

画像をドラッグ&ドロップできるようにしてみよう

最後に1つ機能を追加してみましょう。
下のような機能です。

SimpleMdeコンポーネントにはEasyMDEのオプションをOption propsとして渡すことができます。オプションはたくさんありますが今回使うのはその中のuploadImageとimageUploadFunctionオプションです。

オプションの説明をします

  • uploadImage
    trueに設定すると、画像アップロード機能が有効になります。デフォルトではfalseになっています。

  • imageUploadFunction
    画像のアップロードを処理するためのカスタム関数を設定できます。

よってまずはimageUploadFunctionに渡す関数を実装します。
今回は画像を Cloud Storage for Firebase へアップロードすることにします。firebaseの設定方法は省略します。ドキュメントなどを参考にしてください。

画像アップロードの関数のコードは次のようになります。
処理の簡単な説明をコメントにしてあります。詳しい説明はfirebase Storageのドキュメントなどをみてください。

   // 画像をアップロードする処理
  const imageUploadFunction = (file) => {
    // 保存先の参照を作成
    const storage = firebase.storage();
    const storageRef = storage.ref(`images`);
    const imagesRef = storageRef.child(file.name);
    // 画像をアップロード
    const upLoadTask = imagesRef.put(file);
    // エラー処理や画像の保存が完了した後の処理
    upLoadTask.on(
      "state_changed",
      (snapshot) => {
        console.log("snapshot", snapshot);
      },
      (error) => {
        console.log("エラーが発生しました", error);
      },
      () => {        upLoadTask.snapshot.ref.getDownloadURL().then((downloadURL: string) => {
           // アップロードしたURLを取得してマークダウンに埋め込む
          setMarkdown((preMardown) => {
            return preMardown + `![image](${downloadURL})`;
            });
          );
        });
      }
    );
  };

あとはこの関数をオプションとして渡すだけです。全体のコードは次のようになります。


import React, { useState } from "react";
import SimpleMde from "react-simplemde-editor";
import "easymde/dist/easymde.min.css";
import marked from "marked";
import DOMPurify from "dompurify";
import highlightjs from "highlight.js";
import "highlight.js/styles/github.css";

const MarkdownEditor = () => {
   const [markdownValue, setMarkdownValue] = useState("");
    // ハイライトの設定
   marked.setOptions({
    highlight: (code, lang) => {
      return highlightjs.highlightAuto(code, [lang]).value;
     },
   });

  
  const imageUploadFunction = (file) => {
    // 保存先の参照を作成
    const storage = firebase.storage();
    const storageRef = storage.ref(`images`);
    const imagesRef = storageRef.child(file.name);
    // 画像をアップロード
    const upLoadTask = imagesRef.put(file);
    // エラー処理や画像の保存が完了した後の処理
    upLoadTask.on(
      "state_changed",
      (snapshot) => {
        console.log("snapshot", snapshot);
      },
      (error) => {
        console.log("エラーが発生しました", error);
      },
      () => {        upLoadTask.snapshot.ref.getDownloadURL().then((downloadURL: string) => {
           // アップロードしたURLを取得してマークダウンに埋め込む
          setMarkdown((preMardown) => {
            return preMardown + `![image](${downloadURL})`;
            });
          );
        });
      }
    );
  };
   // エディタの設定
   const autoUploadImage = useMemo(() => {
	return {
	  uploadImage: true,
	  imageUploadFunction,
	};
  }, []);

  const onChange = (value) => {
    setMarkdownValue(value);
  };

  return (
    <div>
      <SimpleMde value={markdownValue} onChange={onChange} options={autoUploadImage} />
      <div
        dangerouslySetInnerHTML={{
          __html: DOMPurify.sanitize(marked(markdownValue)),
        }}
      ></div>
    </div>
  );
};

export default MarkdownEditor;


これでエディタの完成です!!あとは好きなようにカスタマイズしてみてください。

Discussion