Notionページを好きなフォーマットに変換するやつを作った
タイトルは少し過剰宣伝で、実際に作ったものはNotionのページをPandocのASTのjson形式に変換するツールです。
Pandocは様々なフォーマットの文書(.md, .html, .docx, .tex, .epub, .rst, ...)の間を相互に変換することができるHaskell製のツールです。
notion2pandoc -i NOTION_PAGE_ID -s NOTION_API_SECRET | pandoc --from json --to {html,markdown,...}
という具合に使うと、Pandocが対応している好きなフォーマットに変換することができます。Pandocの対応フォーマットはかなり幅広いので、実質好きなフォーマットに変換できると言っても良いのでは無いでしょうか。
経緯
- NotionをヘッドレスCMSとして使うやつをやってみたい
- 公式なNotion APIにはmarkdownやhtmlでページを丸ごと返してくれる機能は無く、ブロックオブジェクトのlistからページを再構成するしか無いみたい
- ちょうどRustを勉強したいと思ってたし、そのためのツールをRustで自作してみようかな
- Notion APIのレスポンスにはネストした構造が色々あったりして、出力部分を自作しようとするとバグらせそうだな
- PandocのASTに変換して、出力はPandocにやらせるようにするか
作ったもの
NotionのAPI referenceとPandoc ASTのドキュメントを見て出てくるデータ構造をひたすら構造体として実装して、あとは変換部分をゴリゴリ作りました。
シリアライズ、デシリアライズについてはserdeが大変に優秀で助かりました。Haskellの世界でよく使われる(らしい)adjacently taggedなjsonへのシリアライズも最初からサポートされているので、シリアライズ部分には殆ど困らなかったです。
デシリアライズに関しては、Notion APIのreferenceが間違っていることがちょいちょいあるのがつらかったです。
面倒だった点
Notionのブロックは場合によっては子要素を持つのですが、これらを一括で取得することはできず、各ブロックのhas_children
を見て必要に応じて再度APIを叩く必要があります。子要素がさらに子要素を持つ可能性もあるので、素朴に実装すると再帰的に呼び出される非同期関数を作る必要があり、Rust初心者にはコンパイルの通し方がよく分からず苦戦しました。ただ、コンパイルエラーがとても親切だったので、それに従いasync-recursionマクロを使うことで解決しました。
NotionのオブジェクトとPandocのオブジェクトはけっこう綺麗に一対一対応していることが多いのですが、微妙な違いも当然あってそのあたりを吸収する必要があります。NotionではParagraphが子要素を持つことができますが、Pandocではできない、などなど。このあたりはNotionのエクスポート機能が吐き出すマークダウンファイルと比較して、同じ挙動になるようにしました(このエクスポート機能をそのまま公式APIとして公開してくれればそれが一番楽だったのですが...)。
Rustの感想
僕はrustlingsをとりあえず終わらせた程度のRust力しか無いので最初は中々苦戦しましたが、頑張ってコンパイルさえ通せば大体期待通りに動くのは気持ちよかったです。ただ、書いたプログラムが本当にRust的な書き方になっているのか不安になることは多くありました。rust-analyzerによるエディタのサポートが強力だったのも心強かったです。
Rustといえば所有権やライフタイムが難しいとよく言われますが、今回作ったプログラムはデータを一方通行で加工していくだけのものなので、そのあたりに詰まることは殆どありませんでした。このあたりを勉強するためにはもうちょっと複雑なアプリを作ってみる必要がありそうです。
やり残し
- テストが無いのをなんとかする(Rustでのテストの作法を勉強する)
- 埋め込みオブジェクトのサポートを充実させる(現状は単なるリンクになってしまう)
- Notionにアップロードされたファイルをローカルにダウンロードする(Amazon S3へのリンクは一定時間経つと無効になる)
Discussion