ReactでMarkdownエディタ(code highlightと画像のD&D upload)を使う

# react-markdown-editor-lite
要件に合うものを探していた結果、これを使ってみることにしました。
Repository: <https://github.com/HarryChen0506/react-markdown-editor-lite>
Demo: <https://harrychen0506.github.io/react-markdown-editor-lite/>
Demo: source <https://github.com/HarryChen0506/react-markdown-editor-lite/blob/master/src/demo/index.tsx>

# 環境
- Windows 10
- node v14.15.4
- PHP 7.2.34(XAMPP) ※ アップロードテスト用

```powershell
npx create-react-app react-app
```

```json
  "devDependencies": {
    "axios": "^0.21.1",
    "highlight.js": "^10.6.0",
    "markdown-it": "^12.0.4",
    "react": "^16.9.0",
    "react-dom": "^16.9.0",
    "react-markdown": "^5.0.3",
    "react-markdown-editor-lite": "^1.2.4"
  },
  "proxy": "http://127.0.0.1"
```

# Code
create-react-appでの生成ファイルがベースになっています。

```public/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>
```

```js:src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />,document.getElementById('root'));
```

```js:src/App.js
import React, {useState} from 'react'
import axios from 'axios';
import MarkdownIt from 'markdown-it'
import MdEditor, {Plugins} from 'react-markdown-editor-lite'
import 'react-markdown-editor-lite/lib/index.css';
import hljs from 'highlight.js';
import 'highlight.js/styles/vs.css'; // highlight.jsの好みのスタイルをここで指定

MdEditor.use(Plugins.TabInsert, {
  tabMapValue: 1, // EditorでのTab入力有効
})

const mdParser = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  // Prismも使おうと思えばここでいけるかな？
  highlight(str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(lang, str).value
      } catch (__) {}
    }
    return ''
  },
})

const App = () => {
  const [text, setText] = useState('')
  const handleEditorChange = ({text, html}) => {
    setText(text)
  }
  const handleImageUpload = async (file) => {
    // テスト用にphpでuploadするコードを書いています(package.jsonに"proxy": "http://127.0.0.1"を追加してXAMPPのディレクトリを見るようにしています)
    try{
      const data = new FormData();
      data.append('image', file)
      const result = await axios.post('/test-image/Upload.php', data, {headers:{'Content-Type': 'multipart/form-data'}})
      return new Promise(resolve => {
          resolve(result.data.filepath)
      })
    }catch(error){
      // error dialogとかlogとか(未実装)
      console.log(error)
    }
    /*return new Promise(resolve => {
      const reader = new FileReader()
      reader.onload = data => {
        resolve(data.target.result)
      };
      reader.readAsDataURL(file)
    });*/
  };
  const renderHTML = (text) => {
    return mdParser.render(text)
  }
  return (
    <MdEditor
      style={{ height: "600px" }}
      renderHTML={renderHTML}
      onChange={handleEditorChange}
      onImageUpload={handleImageUpload}
      /*onCustomImageUpload={handleImageUpload}*/
      config={{
        view: {
          menu: true,
          md: true,
          html: true,
          fullScreen: true,
          hideMenu: true,
        },
        table: {
          maxRow: 5,
          maxCol: 6,
        },
        syncScrollMode: ['leftFollowRight', 'rightFollowLeft'],
        imageAccept:'.jpg,.png',
      }}
    />
  )
}

export default App
```

```php:Upload.php
<?php
/**
 * file uploadに対するチェック処理を一切していません。Local以外では使わないでください。
 */
if($_SERVER["REQUEST_METHOD"] != "POST") return;

$uploadDir = dirname(__FILE__) . '/upload/';
$uploadfile = $uploadDir . basename($_FILES['image']['name']);
if (move_uploaded_file($_FILES['image']['tmp_name'], $uploadfile)) {
    $isSuccess = true;
} else {
    $isSuccess = false;
}
$result = [
    "success" => $isSuccess,
    "filepath" => '/test-image/upload/' . $_FILES['image']['name'],
];
echo json_encode($result);
```

exampleがclassコンポーネントで書かれていたのですが、ナウなヤングはHooksで関数コンポーネントで云々(ルシのファルシがレベルで意味はわかっていませんでしたが、昔はclassじゃないとstateを保持できなかったとかなんとか)らしいのでなんとなくclassを使わずに書きました。

![md_test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/595802/9a72d643-dffe-ae17-6b4d-b46b5312e6ad.gif)

今気づいたのですが、不要なツールバーアイコンを無効にする、等といったことが容易にできない？

# 旅
WordPressの代替となるオレオレブログ(Go製)を作っているのですが、管理側はSPAにしようと試行錯誤しています。ReactというかJavaScriptについては明るくないので、あまり弄らなくても済み、尚且デファクトスタンダードなもの(が有れば)を使う予定でした。が、どうもピンとこないので色々なものを触ってみることに。

- react-mde
preview分割がない。あとせっかくmaterial ui使っているのでデザインもう少しなー(デザイン全然ダメなので)。ここから旅が始まる。

- react-split-mde
"react markdown editor preview split"などでググったら速攻で日本語エントリがhit！しかしbuildは通るが実行時にエラー吐いて落ちるので断念。おま環なのか、blogでは未完成とのことなのでそれなのか。

- for-editor
npm install時にauditさんが怒った。toolbar周りが中国語で「えーこれはなんだ？」と中国語翻訳とかやりだして脱落しそうになったが、ソースを斜め読みしたところlanguage="en"にすればとりあえず英語表記にできるようなので続行。ってreadmeの属性のところに書いてありましたね……！英語以上に中国語は脳がAutoスキップしていた。しかし、この時点で欲が出ていて画像のD&D upload欲しいよな、となったので離脱。

- react-simplemde-editor
左右分割したいだけなのに、ブラウザ全体に領域拡大(side by side)するのが要件に合わず。もっと調べればやり方はあったのかも。

- markdown-to-jsx
エディターではなかった。自前で作るのは無しの方向で。このあたりで「や、自分しか使わないんだからTextAreaベタっと貼ればよくない？」という考えが脳裏をよぎる。

- @toast-ui/react-editor
componentを置いたところイメージが「これだー！」となったけど、onChangeとか無くてよくよく見るとmdをhtmlにformatしたあとの値を云々するものかな？

- react-markdown-editor-lite
googleの画像検索で"react markdown editor"で見つけた。確か。demoサイトで画像uploadもダミー実行できたので見てみることに。「おー、簡単だぞこれ」ということで採用。一日中探しながら試して疲れたのもあり。

# 誤り等があれば
2年ほど前にS3(React), Lambda(C#), DynamoDBという構成で社内ツールを作ったことがある、というレベルの人間です。それ以外だと過去に仕事でOSSのCMSやECommerce等のJavaScript(殆どJQuery)を改変していたくらいで、体系的に学んだこともなければ趣味でも殆ど書きません。実装面で誤りがあれば指摘いただけると幸いです。このエントリで言えば画像のuploadのところ。onImageUploadはPromiseを返す(返さないといけない)関数なのだけど、axios.POSTもPomiseを返す。なんか美しくない気がするので他に書き方があるのかもしれないけど思いつかず。