👌🏻

自作のアウトライナーに Markdown 互換ビューを実装

2022/06/12に公開

デモ

実際の動作を確かめたい場合はこの JSON データここからインポートして、ここにアクセスしてください。
右上に設定アイコン郡が並んでいるので、show/hide indentshow/hide bullethideにすればマークダウンっぽい見た目になります。

はじめに

前回、アウトライナーを自作したという記事を書きました。
https://zenn.dev/k4a/articles/introducing_twirliner
この記事も該当のアウトライナーを利用して書いたのですが、アウトライン構造からマークダウンへ変換する作業が地味に面倒でした。

マークダウン形式でエクスポートする機能はつけているのですが、あくまで機械的にヘッディングなどを生成します。
アウトラインに適した文章構造とマークダウンに適した文章構造は微妙に異なるため、エクスポート後に整形する作業が必要でした。

アウトライナー構造とマークダウン構造の差異

アウトライナー構造

アウトライナーでは構造自体が文章の意味を補完するので、ぶつ切りの文を書きがちです。
「タイトル」という概念は存在しないので、普通の文に別の文を付属させてしまいます。
コンテキストの継続する文章を前の文章に付属させたくなるので、インデントが多くなります。

- 初期描画の問題はある程度解消されましたが、再描画時に問題が残っていました。
	- 初期描画では数100ms程度待たされても我慢できます
 		- 更新のたびに待たされるのは苦痛です
 	-React を代表とする仮想 DOM は、宣言的な UI 作成を実現しています。
		- javascript のオブジェクトを宣言しておいて、それに合わせてDOM を作成する
  	- これは非常に優れた概念で、バグを生みにくく変更に強いシステム開発が可能であることは間違いありません。
  	- しかし、アウトライナーのように多重で深く再描画が多い構造には不向きな点があると感じました。
   		- 私の知識が乏しいため、宣言的な書き方だけで実現できる可能性はあります。

マークダウン構造

マークダウン(または通常のテキスト形式)では、短いタイトルを作ってそこに数行の文章ブロックを付属させます。
弱い親子関係の場合はわざわざタイトル+ブロック化はせず、対等な文章として並べるか、改行を利用して擬似的なブロックを表現します。

# 再描画の問題

初期描画の問題はある程度解消されましたが、再描画時に問題が残っていました。
初期描画では数100ms程度待たされても我慢できますが、更新のたびに待たされるのは苦痛です

React を代表とする仮想 DOM は、javascript のオブジェクトから DOM を作成する宣言的な UI 作成を実現しています。
これは非常に優れた概念で、バグを生みにくく変更に強いシステム開発が可能であることは間違いありません。

しかし、アウトライナーのように多重で深く再描画が多い構造には不向きな点があると感じました。
(私の知識が乏しいため、宣言的な書き方だけで実現できる可能性はあります。)

マークダウンに適した形にしないまま変換した場合

アウトライン構造をそのまますると長い文章が Heading 化され、数もやたらと多くなります。

そのまま変換

# 初期描画の問題はある程度解消されましたが、再描画時に問題が残っていました。


## 初期描画では数100ms程度待たされても我慢できます

更新のたびに待たされるのは苦痛です

### React を代表とする仮想 DOM は、宣言的な UI 作成を実現しています

javascript のオブジェクトを宣言しておいて、それに合わせてDOM を作成する
これは非常に優れた概念で、バグを生みにくく変更に強いシステム開発が可能であることは間違いありません。

### しかし、アウトライナーのように多重で深く再描画が多い構造には不向きな点があると感じました。

私の知識が乏しいため、宣言的な書き方だけで実現できる可能性はあります。

仕組み

新規ビューの追加

前回の記事で紹介したビューに「:markdown」を追加し、そのビュー内で記述をした場合に擬似的なマークダウン構造で表示を行うようにしました。

文章構造は、子要素を持つ行は Heading として扱い、それ以外の行は通常の文章として扱います。

ただし、Heading ではない「リスト」を表現したい場合もあります。
アウトライナーでは全てがリストなので Heading と List の区別ができません。
block ビュー内にプレーンテキストで記述をすれば一応そのまま出力はできますが、せっかくアウトライナーを使っているのにプレーンテキストを織り交ぜるのはクールではありません。

そこで、Ordered Lists として設定した行は Heading 化せずに子要素を全てリスト化するようにしました。

具体例

アウトライン構造
- アウトライナー
	- アウトライナーは全てを行として扱います
		- なので、改行は存在しません
	- アウトライナーは行を折りたたむことができます
		- なので、表示される情報量を常に変化させることができます
	- アウトライナーアプリの例
		1. Dynalist
			1. 私はこれを主に使用しています。
		1. Workflowy
		1. Roam Research
markdown への変換
# アウトライナー

## アウトライナーは全てを行として扱います
なので、改行は存在しません

##アウトライナーは行を折りたたむことができます
なので、表示される情報量を常に変化させることができます

アウトライナーアプリの例
1. Dynalist
	1. 私はこれを主に使用しています。
1. Workflowy
	1. Roam Research

インデントの隠蔽

インデントを隠す設定機能を追加しました。
これによってアウトライナーの操作感を保ったまま、ほぼマークダウンと変わらない見た目を編集できるようになります。

使い心地

びっくりするくらい便利です。
私のワークフローをツール化しているので私に合致するのは当たり前なのですが、この仕組みだけでもツールを開発した価値があったと思います。
(既存のアプリでも同じようなことはできるとは思います)

マークダウンは、タイトル(ブロック)の深さをわざわざ指定しないといけない点が気に入りません。
前のブロックより深くしたいだけなのに 3 階層目か 4 階層目かを気にしないといけないですし、親の深さを変えた場合は子の深さも変更しないといけません。

アウトライナーではそんなことを気にしないで、どこに所属させるかだけを決めれば良いので内容だけに集中できます。
とはいえマークダウンに出力したときのことも考えて文章構造を作らないといけません。

今回の実装によって、アウトライナーの利点を享受しつつマークダウンの構造を作ることができるようになりました。

今回の記事も、Zenn 用のメタデータを手動で書き加えた以外は、エクスポート後に全く手を加えず公開することができました。
ただ、アウトライナーだと句点毎に行を区切るのが普通なので、そこは少し読みづらいかもしれません。
場合によっては改行させる行とそのまま繋げる行の調整が必要になりそうです。

課題

Headings と Lists

「Ordered Lists は全て普通のリストとして扱う」というルールが本当に良いのかが微妙です。

マークダウンからの再変換

マークダウンを再編集したいときに、アウトライン構造に再変換する機能はまだ作れていません。
正しく再変換ができない場合初期執筆にしか使えないので利用価値は低くなりそうです。
アウトライナー構造のままマークダウン表示を作れるツールより、マークダウン構造のままアウトライナー操作ができるツールのほうが良いのかもしれません。

Discussion