Zenn記法のマークダウン -> WYSIWYGのHTML への変換
前回、ZennのWYSIWYGエディタを開発しているという記事を公開しました。
それからも開発を続けていますが、テストも兼ねて開発した機能の記事を執筆します。
今回紹介する機能は、Zenn記法のマークダウン -> WYSIWYGのHTML の変換です。
逆の方向は既に実装されていたのですが、こちらは実装方針にかなり悩みました。
サービス
Zenn記法のマークダウン -> WYSIWYGのHTML の用途
WYSIWYGエディタを活用する時は、新規で記事を書き始める時だけではないです。
-
公式ZennエディタやVSCodeで書いた記事を、本エディタで編集したい
-
WYSIWYGエディタで書いた記事を公開、再編集したいとき
など、マークダウンをHTMLに変換したいことは多々あります。この機能はユーザーが利用する上で、絶対に必要と考えていました。
困難ポイント
ですが、この実装に非常に頭を悩ましていました。。。
最初は、Tiptap のPasteRule で InputRule と同じようにできるやろ〜と気楽に考えていましたが、期待はすぐに打ち砕かれました。
Input と Paste は何が違うのか?大きく2つあります。
-
Input は一行で完結するので正規表現で簡単に識別できるが、Paste は複数行を認識して変換する必要がある
-
Input は入力テキストだけをを正規表現で確認すればいいですが、ペーストは
text/plainとtext/htmlの2パターンの入力がある
まず1つ目は、Pasteは複数行のパターンを認識して変換する必要があることです。例えば、以下のマークダウンがあるとします。
:::message
## h2
この中はメッセージです。。。
:::
:::message ... ::: というメッセージブロックに囲まれて、見出しと段落が挿入されているという意味です。
特定の構文の中で、更に内部でネストされる可能性がある。。。この構造、どっかで見たことがあります。そう、コンパイラの構文解析です。
よくよく考えると、マークダウンのHTML変換は markdown-it といった専用のパーサーを用いることが多いです。なので、付け焼き刃の正規表現で変換をしようというのが、思い違いの1つでした。
2つ目は、実はペーストは2種類あることです。普段はあまり意識しないのですが、text/plain と text/html があります。(厳密には任意で種類を指定可能)
例えば適当にWeb上でテキストをコピーしたとします。この時、クリップボードの中身はClipboard Inspectorで確認するとこのようになります。

コピー範囲

Clipboardの中身
text/plain はテキストオンリーですが、text/html はスタイルで呪文のようになってます。
重要な箇所だけ抜き取ると、h2 と p の2つが浮かび上がります。
<h2>WYSIWYGエディタについて</h2>
<p>
そもそも<span> </span>
<strong>WYSIWYG エディタ</strong>とは何か?ということがありますが、Notion のような最終成果物をその見た目のまま編集できるエディタになります。
</p>
他はこれらを装飾する情報やメタデータです。
元の書式を維持して貼り付けというのがありますが、それはこの text/html を元に貼り付けているということです。
Tiptap(ProseMirror) はこの2つのデータがあったときに、text/html を優先的に読み込みます。
この時、貼り付け対象をマークダウンとして認識したいか?エディタ内のノードコピーのようにHTMLとして認識したいか?テキストにリンクの貼り付けをしたいのか?の判断が困難という結論になりました。
考えた末に...
Paste で上手に実装することは難しいと考えて、貼り付け対応は一度諦めました。
代わりに、マークダウン入力をして文書全体を置換するフローを導入しました。

左上の Mボタン を押すとマークダウン入力ダイアログが出現するので、ここにマークダウンを入力するとWYSIWYGエディタに反映されます。
仕組みは、マークダウン -> zenn-markdown-htmlのレンダリング -> TiptapのparseHTML で編集可能ノードにしています。
zenn-markdown-html は、Zennが公式に提供しているレンダリングツールです。
内部的には markdown-it を使っています。
ただ、悲しいことにNodeでしか動作しないとのことでした。。
調べてみると、Nodeでしか対応していないcryptoモジュールの使用や、CommonJSへのビルドなどが原因ぽかったので、フォークして少し修正することにしました。
zenn-markdown-htmlを活用することで、マークダウンのパース処理を一括で任せることができました。公式で公開してくださってありがたい。
Tiptapでは、HTMLのパース設定をZennのHTMLに寄せてることで対応してます。
最後に
今回の実装でクリップボードやパーサー、ビルド周りの挙動の勉強になリました。
まだ完成してはいませんが、執筆をして実用レベルにはなってきていると実感しています。
Notionみたいにブロック単位で選択・ドラッグ&ドロップできると、使い勝手が100倍違いました。
とりあえず破壊的変更の必要がなくなる安定動作を目指して進めます!
Discussion