GROWI に draw.io 連携機能を PR (Pull Request)して v3.7.0-RC としてリリースされた話

# はじめに

[GROWI](https://growi.org/) に draw.io 連携機能を実装して Pull Request し、無事マージされて、バージョン 3.7.0 RC(Release Candidate) としてリリースされました！ :tada: (今は docker image のみ提供されてます)

[weseek/growi Add draw.io Integration #1685
](https://github.com/weseek/growi/pull/1685)

使い方のドキュメントはこちらです。

[draw.io で様々な図を作成する | GROWI Docs](https://docs.growi.org/ja/guide/features/drawio.html)

この記事では簡単な機能紹介と実装のモチベーション、どうやって実装したかなどをお話しようと思います。

# GROWI って何？

[株式会社 WESEEK](https://weseek.co.jp/) が OSS として開発している Markdown で書ける Wiki です。

* 公式 HP
    * https://growi.org/
* ドキュメント
    * https://docs.growi.org/
* クラウド(SaaS)版
    * https://growi.cloud/

技術スタックは下記の通り、わりと鉄板な構成です

* React
* webpack
* Express
* MongoDB
* etc ...

# どういうことができるようになったの？

PR に掲載しているアニメーション GIF を置いておきます。

![75884338-f7304680-5e67-11ea-929d-b7614b8d671a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19717/9a3c8cac-3c34-bf25-d36b-63a8d3f8d3bb.gif)

端的に言うと下記の項目が一通りできるようになりました。

1. 編集画面から、ツールバーの draw.io アイコンをクリックすることで図を追加できる
2. 追加した draw.io の図がページ表示画面で表示できる
3. draw.io の図を2通りの方法で編集できる
    * ページ表示画面で「編集」ボタンから図の編集ができる
    * 編集画面で画面左側にあるエディタ上で Base64 エンコードされた箇所にカーソルを合わせて、ツールバーの draw.io アイコンをクリックすることで既存の図が編集できる

# 作ったモチベーション

GROWI には元々 [PlantUML で UML 図を描画できる機能](https://docs.growi.org/ja/guide/features/uml_diagrams.html) や [blockdiag で図を描画できる機能](https://docs.growi.org/ja/guide/features/blockdiag.html) が搭載されていました。

「これでめっちゃドキュメント書き放題やんけ！」と思っていたのですが、やはり記法を覚えないといけなかったり、自分で図のカスタムを行いたいときの不自由さが目につきました。

また、 Twitter や Qiita で GROWI の反応を見ると、作図機能を使おうと試行錯誤してくれている方は結構居るものの「使いづらいかもしれん ... (´・ω・｀)」という気持ちが湧きました。といいつつ、blockdiag の図を生成する機能を追加したのも自分なんですが。 ([PR はこちら](https://github.com/weseek/growi/pull/458))

* [Excel設計書を抹殺したくて4年前にWiki設計書を導入したら、意外とちゃんと開発回ってた話。](https://qiita.com/gakuri/items/93b06fb47eb538d2e447)

アレコレ考えるうちに、「図はやっぱり見たまま編集できる (WYSIWYG) 方法が一番だろ！」という至極シンプルな理由で実装することを決めました。

また、実装中何度も心が折れかけましたが、 Qiita で draw.io の使い方の解説記事が公開されて「これは実装しきるしかない！」と後押しされた、というのも最後まで完成させる大きなモチベーションになりました。

* [【動画付き】 draw.io 使い方まとめ 〜エンジニアでなくても使えるTips集〜](https://qiita.com/G-awa/items/8fd414700b68b2bcafcc)

また、 draw.io の図が GROWI で管理できるようになると、ページの「更新履歴」タブから過去の図を復元できるため、GROWI の仕組みを上手く活かせるのも嬉しいポイントですね :four_leaf_clover:

# 完成に至るまでの道のり

ここからはどのように実装していったかの経緯を書いておきます。

## 1. draw.io の図をテキスト形式で表現できないか調べる

GROWI の編集画面は WYSIWYG 形式ではなく、テキスト形式を想定していますので、Markdown 内にバイナリデータを埋め込もうとしても、その表現方法がありません。
そのため、テキストで埋め込む形式をまず調べる必要があります。Google さんにお尋ねしてもあまりいい答えは返ってこなかったため、 draw.io を提供している jgraph の github を調べることにしました。

* https://github.com/jgraph

そこで、[jgraph/drawio-integration](https://github.com/jgraph/drawio-integration) という目的に合ったものが見つかったため、このソースコードを見つつ、テキスト形式で表現できることが分かりました。

コードでいうと [ここらへん](https://github.com/jgraph/drawio-integration/blob/master/localstorage-svg.html#L153)

大分ざっくり話しますが、バイナリ形式を Base64 化した形式で表現していることが分かります。

## 2. draw.io の仕組みについて調べる

draw.io の図のサンプルを見ながら、データ形式を探っていきます。

* [draw.io coffee example.xml](https://github.com/jgraph/drawio-diagrams/blob/master/training-diagrams/draw.io%20coffee%20example.xml)

さらにデータ形式を解釈して図として変換してくれる JS が draw.io のコードのどこに該当するかを見つけるために、コードを読み進めていきます。 [GraphViewer.js](https://github.com/jgraph/drawio/blob/796485a22b543c22d39e6cc3fb47504cbb7c6440/src/main/webapp/js/diagramly/GraphViewer.js) というものがデータを解釈して、図として変換してくれることが分かったため、この JS を使ってプレビューを行うことが分かりました。

実際にビルドされたソースは [viewer.min.js](https://github.com/jgraph/drawio/blob/796485a22b543c22d39e6cc3fb47504cbb7c6440/src/main/webapp/js/viewer.min.js) という最適化された形の JS です。

ここまで分かれば「特定の HTML タグに Base64 化されたデータを埋め込んで、 viewer.min.js に食わせれば
表示ができる」というところまで到達できます。(実際もっと途方も無い時間がかかってますが)

その後に小さなプロトタイプを HTML と JS で作り、自分の仮定が上手く行っていることを確認しました。

## 3. markdown-it が draw.io のデータを食って、特定のタグを吐けるような npm パッケージ を作る

プロトタイプ実装後、 GROWI にすべての担当をお任せすることはコード量が増えることを懸念し、 「draw.io のデータを食わせると、特定のタグを吐かせる markdown-it のプラグインを担当」する npm パッケージを作りました。かなり端折ってるので、詳しく知りたい方は質問していただけると。

* ソースコード: https://github.com/kaishuu0123/markdown-it-drawio-viewer
* 実行サンプル: https://runkit.com/embed/c8zmotx8yypi

入力

```xml
::: drawio
(ここに draw.io のデータ(Base64 エンコードされたもの)が入る)
:::
```

出力

```xml
<div class="drawio-viewer-index-0 markdownItDrawioViewer"
  data-begin-line-number-of-markdown="2"
  data-end-line-number-of-markdown="6">
  <div class="mxgraph" style="max-width: 100%; border: 1px solid transparent" data-mxgraph="{&quot;editable&quot;:false,&quot;highlight&quot;:&quot;#0000ff&quot;,&quot;nav&quot;:false,&quot;toolbar&quot;:null,&quot;edit&quot;:null,&quot;resize&quot;:true,&quot;lightbox&quot;:&quot;open&quot;,&quot;xml&quot;:&quot;&lt;?xml version=\&quot;1.0\&quot; encoding=\&quot;UTF-8\&quot;?&gt;\n&lt;mxfile userAgent=\&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36\&quot; version=\&quot;6.8.9\&quot; editor=\&quot;www.draw.io\&quot; type=\&quot;atlas\&quot;&gt;&lt;mxAtlasLibraries/&gt;&lt;diagram name=\&quot;Page-1\&quot; id=\&quot;bac0fdb4-bee4-c619-222c-02e826218fb6\&quot;&gt;1VtNk5s4EP01vqYQAhsfY2eS3UMqqfJu7eaowQooAeQI4Y/99SsGgXELJw7GoPHBhRrRkl4/ulste4bX6fGDILv4I9/SZOY62+MMv5u5brBcqO9ScKoEfhBUgkiwbSVCZ8GG/Ue10NHSgm1pftFRcp5ItrsUhjzLaCgvZEQIfrjs9pUnl6PuSKRHdM6CTUgSanT7h21lrJflt3r/QVkU1yMjR995JuH3SPAi0+PNXPz15VPdTkmtS/fPY7Llh5YIP83wWnAuq6v0uKZJCW0NW/Xc+yt3m3kLmslbHnCrB/YkKfTS3zESCZLq6clTDckhZpJudiQs2wdl9Rle5VLw7w1CWElimSbqEqlLcyp6dnsqJD22RHpqHyhPqRQn1UXf9eYaJk0jTzcPZ5ugGsm4ZY/6MaJpEDWaz1CoC41GNzLYQGYjRRHKQtCZO1d2VVzFK5vQat4ejRYaEy7PgGtFY7JnvBCWwhVMCZdvwLVOSJ4rkU0QLS8Rcv0REZqbCPF0x3OFQotQtr+TgGM4GBHBhYHgZxWdyshnJ1YYTwhWYAZCukv4KS3XYydeaDEhXsvu1zOzF67Ge03hzWo/0MLr0/O3MnW1EywPTQkWMsB6G0q2Z/JkKVx+MCVcZhL/ZyapIAoznlmK2AJPiZiZ3P+dq7DorElua3QMJqWYmd5vJCkzMecjCWOW2YoaciblmZnmb+iPgmahrXhBRzZqToE6c/60yFhIXpErGxczM8vvdv6f1IL2jB4sBRG6t3FBNLP/v1jKsshSsAyvNi5aZu5voEK3Ed3oJk2e+eHpLFi9CNSNcsHq1VbjrF5KprQcwLkRrZwXItSj6XAuiYio7qWnVM7jp4i2AXM6AGuEgibKCe0vC8RdMOoxPnNWboWulFKavVutoVqOfuhsC0MPAoqgngoDQ8+LTZtl32Tmmj72mNmz38w+LJm5Pe08h4SBigY0tLndmtjQvmlobJmhYVqJ+77PQBGCiq7Y+a0Q5NTqtis75Nfni2D14/JgSF1UGnuTyNyE3k0iemTy35JCb3zd+qIJpcgjTq1bZfOLZth91Fu+Ouo15xb3Ug/qGYh5xinC0MwzN/MTu6+6dmYzifyh8hG/Vz7yuyTCiweTyCxwTEyiuf0cgo5oOZAjgnqGCoGPdkRmtedVhsDAfuYBS/q9mQcUeQ+innH2NDT1zMrZ/dT7CcGu0/I+6i3spx50erCi0tfpQT1DBU7vwcwz648TB8769LBNIs8yEvngh21u0JNFc6AIQUUD0WgBJzw0jcwK7NQ06qg22UYjD54J9o2D8NjHSOUGotESTnhoGj2gND1FHKyP9G0mn3/tZPW3fRgMqVDRQOQL4IQHJl+t/tWTr+M4xTryOQN5vrkzkueDP6gamnzWFfFRRxW/9mvW8MiDO8m+Tgwe/Hg3OrE+pnatM3VHsco6U89B7uH33bktgCLvxq1bH1PbV9vu2KLbZ2o0lKnReKa2rgKNOgqB1pka+l2/904aOvAbd9K/NrVqnv/QV3U//2kSP/0P&lt;/diagram&gt;&lt;/mxfile&gt;&quot;}"></div>
</div>
```

## 4. GROWI に実装

この実装を行う際には GROWI のメンテナである @yuki-takei さんに GROWI でのレンダリングの仕組みなどを教えていただきつつ、実装を行いました。ありがとうございます :sparkles:

* draw.io エディタの起動方法を調べて、iframe 内で draw.io エディタを表示して `position: fixed` で既存画面に被るように表示し、 iframe と GROWI 間でデータが受け渡せるように実装したり
* 不用意に再レンダリングが走らないように GROWI の lsx プラグインなどと同様に draw.io の図を React コンポーネント化したり
* Table 編集機能と同じようにページ表示部分から「編集」ボタンを押すことで、図の更新ができるようにしたり
* 既存の仕組みに乗っかるようなコード構成に大修正したり

と GROWI に組み込む際にもそこそこのコード量になりました。( [PR の内容](https://github.com/weseek/growi/pull/1685/files) )

アドバイスもあり、とてもシンプルな形で機能を追加できたんじゃないかなと思っています。

# 今後の課題

1. やっぱり図が大きくなればなるほど、レンダリングが遅くなるんじゃないか、という不安
    * これは実際に使ってみてもらってから様子を見ればいいかな、と思っています
    * 最悪、図を他のページに分割して保存するという方法で回避していただく方向で ...
2. エディタ画面に Base64 エンコードした文字列がそのまま書かれるのは分かりづらい
    * これはエディタの折りたたみ機能を使って、なるべく Base64 エンコードされた文字列が意識されないようにできるといいかなぁと思っています。
    * 本当は添付ファイルを CRUD できる機能が GROWI にあれば、 draw.io の図は画像みたいな取り扱いができるっちゃできるんですが、それはそれで実装コストが高すぎる気がするので

# 最後に

draw.io の仕組みを理解するまでに結構な時間がかかりましたが、 Markdown に draw.io の図を埋め込める機能が作れた経験は貴重でした :dizzy:
また、 draw.io に関する様々なノウハウを得ることができました。(データ構造や viewer.min.js の仕組みなどは今回の記事ではめちゃくちゃ端折っています)

Visual Studio Code の拡張機能に応用できたりするかもしれません。

皆さんも是非今回実装した機能を使ってみてください :grinning: