✏️

ZennのWYSIWYGエディタを作ってみた

に公開

Zenn の記事を執筆するとき、どのような方法で始めますか?

Notion や手元のコードエディタで一度書いてから貼り付ける方もいると思いますが、私は Zenn のエディタ上の書き心地が好きだったので、Web 上で直接書いていました。

しかし、使っていると細かいところが気になり始めます。例えば、

  • マークダウンと表示の切り替えにタイムラグがある

  • 切り替え時にスクロールが合わない

  • 編集時に特定ブロックのハイライトがされず、文章が長くなると見辛い

他のエディタで一度書いてから貼り付けでもいいのですが、対応していない記法があるなど見た目をすぐに確認するのが少し手間でした。

そこで最近リッチテキストエディタを触っていたこともあり、Zenn に対応した WYSIWYG エディタを開発してみました。

制作物

まだ開発途中で対応していない機能がありますが、こちらになります。

初期コンテンツで機能説明があり、編集可能なので試してみてください!

https://zenn-wysiwyg-editor.karintou.dev/

https://github.com/karintou8710/zenn-wysiwyg-editor

WYSIWYGエディタについて

そもそも WYSIWYG エディタとは何か?ということがありますが、Notion のような最終成果物をその見た目のまま編集できるエディタになります。

詳しい説明は meijin さんや irico さんの素晴らしい記事に投げます!

https://zenn.dev/meijin/articles/rich-text-editor-basis-knowledge

https://zenn.dev/cybozu_frontend/articles/7733bf0560829e

実装方針

最近リッチテキストエディタ界隈で波に乗っている Tiptap を採用しました。

実装は Zenn のマークダウン記法を公式ドキュメントで参照しながら実装を進めました。

ここで、Zennの記事スタイルはありがたいことにGitHubで公開されているので、読み込むだけで見た目が整います。(感謝です)

具体的なノードの構造は、開発者ツールで覗きながらコツコツやりました。

見出しの実装

見出しを実装したい!ということになれば、Tiptap が提供しているHeadingを拡張していきます。

Zenn はh1~h4を採用しているので、それのみ対応できるように Heading ノードを拡張します。

heading.ts
Heading.configure({
  levels: [1, 2, 3, 4],
})

Tiptap がレベルを設定できるものを提供してくれているので、これだけでマークダウンのショートカット記法・パース・レンダリングなどが使えるようになります!

Zenn のスタイルが設定されているため、これだけで見た目がいい感じになります。

他にEnterBackspace周りの挙動を、好みに拡張していく。。といった感じです。

コードブロックの実装

複雑なブロックだと、コードブロックがあります。

ZennはコードのハイライトにPrismを使っていますが、これを Tiptap のライフサイクルに紐付け・編集可能にする必要があります。

こちらもありがたいことに、Tiptapが公式にlowlightのハイライト対応したコードブロックを公開してくれています。さらにこの公式実装を参考にしたprismのコードブロックも有志が公開されていたので、これらを参考に実装しました。

しかし、Zenn はファイル名や差分ハイライト表示に対応しています。これらの参考実装はないため、一から実装します。

例えばファイル名は、左上に常に編集可能オブジェクトを配置する方針にしました。


編集可能なコードブロック

一見1つブロックを追加するだけですが、かなりの曲者です。以下の挙動を考える必要があります。

  • コードブロックの先頭で Backspaceを押した時、ファイル名を含めて全体を削除する

  • ファイル名の先頭で Backspaceを押すと、何も起きない(削除されない)

    • 同様に Enterを押した時も考慮する必要あり
  • ファイル名を追加することでコンテナノードが必要になるが、加わることでズレるカーソル位置の処理

これらはファイル名とコードブロックの関係性が密接になることで起きます。

エディタはブロックの関係性を詳しく知らないので、それらを自分で定義します。

(ファイル名とコードブロックを跨ぐ選択範囲の削除など、探せばまだまだバグありそう。。。)

Markdown出力

このエディタが出力するHTMLを直接 Zenn に貼り付けることができないため、一度マークダウンにします。

出力には prosemirror-markdown を採用しました。

markdown.ts
blockquote(state, node) {
  state.wrapBlock("> ", null, node, () => state.renderContent(node))
},

こんな感じで対応するブロックのテキスト出力を書いていきます。

今後について

アコーディオン、テーブル、埋め込み要素、マークダウン貼り付け... など、まだ対応していない機能が多いです。

これらの対応を進めていき、最終的には記事執筆の普段使いにできるレベルまで持っていきたいと考えています。

最後に

この記事は開発したエディタ上で執筆しましたが、想像の 10 倍書きやすくて自分で驚いてます。

WYSIWYG エディタ開発は、今まで見ることしかできなかった箇所が編集できるようになり、他にはない楽しさがありました。

まだまだバグや未対応の機能も多いですが、触っていただけると嬉しいです!

Discussion