markdownで書かれたドキュメント(画像付き)を良い感じに扱うツールを作った

github.com

markdownで書かれたドキュメント(画像付き)を良い感じに扱うツールを作った。名前は「書斎」から来ている。

ほしかった理由

ほしかった理由をまじめに書くと、ドキュメントにはフロー型のものとストック型のものがあり、現在の手元のツールではストック型のものに対応できない。 もっと大掛かりなブックコンパイラーのようなもの(e.g. sphinx)は導入のコストが大きい(個人的には、sphinxなどで生成されるようなある特定の話題についてこれを読めばOKという程度にまとめたものをbookと呼び、一枚限りの文書であってもずっとメンテをし続けるストック型のものをdocumentと呼び、一回発表したらおしまいと言うようなフロー型のものをmemoと呼んでいる)。

あるいは、それを利用するにしても生成したドキュメントをどうやって共有するかという問題があった。情報共有のインフラが決まっている場合、そのインフラに乗るほうが良い(例えば、docbase,kibela,esa,qita-teamのどれかを使っているならそのどれか)。

そしてストック型のdocumentをどうやって取り扱うかと言うのが悩みの種だった。

web uiに一切触りたくない

個人的には、ドキュメントを更新する時にweb uiに一切触りたくない。これは自分が優秀ではないせいかもしれない。優秀の定義は色々あるけれど、ここでの優秀さの定義はミスの発生率が少ないというようなもの(質やスピードなどその他の尺度も存在するが今回は脇に置いておく)。具体的に言うと、ここで言う優秀とはちょっとした誤字脱字などの修正が少ないということ。

そして、自分自身に限って言うと、誤字脱字が非常に多い。なので何らかの文章を書くときには、追記追記追記と追記に次ぐ追記を繰り返すことになる。その上実質誤字脱字の修正がほとんど。もちろん書き忘れなども多いためそれらの修正も行われる。

つまるところ対象の文章に対する軽微な修正がものすごく頻繁に発生する。これをweb uiで取り扱うのが面倒というのがことの発端(たいていのサービスではドキュメントの編集部分のUIが比較的リッチな機能になっていることが多いので、ブラウザでアクセスしたときに感じる精神的負荷が大きいという話もある)。

フロー型とストック型

今までも幾つか自分自身のメモを投稿するためのツールを書いてきていた。

-過去に作ったgoのコードを書き直して、hatenaのアップローダーにprofileオプションを追加したりした - podhmo's diary

上の記事はhatenaの投稿用のツールなのだけれど。これと同様のUIのgist用のツールも作っている。すごく雑に言うとこの投稿用のツールは以下の様な使いかた。

# alias無しあるいは新規のaliasは新規投稿
$ <command> memo.md
# defaultで利用されるaliasはheadなのでaliasにheadを指定すると更新(追記)
$ <command> -alias head memo.md
# 追記
$ <command> -alias head memo.md

一度書いた文章をuploadしそれに対して追記を繰り返すという形で使われる。このようなUIはフロー型の情報の扱いには便利なのだけれど。ストック型の情報については上手くない(上の例で言えば、各自異なったaliasを指定すれば、過去のドキュメントについても更新を書けることはできるのだけれど、たいていの場合で惰性でheadをaliasに使っていてそれができない。あるいは過去のaliasなんて覚えていないみたいな話もある)。

今回はストック型の情報を管理したいということで別のツールを作った(ストック型フロー型の情報についてやbook,document,memoについてはどこかで詳しく話したいような気持ちもある)。

コンセプト

今回のツールのコンセプトは以下。

  • 追記(既にある記事の更新)のためのツール
  • 手元のファイルをバウンダリーオブジェクトとしてサービス上のドキュメントと同期

色々な理由により現在はdocbaseだけサポートすることにした。

使いかた

shosaiというコマンドが使えるようになる(現状pypiにはあげていないがいずれできるようにする)。

$ pip install shosai
$ whichi shosai

追記のためのツール

追記をしやすくがコンセプトなので、すでに存在しているドキュメントをいかにして更新するかという部分を気にしていた。(なので各サービス毎に存在する権限だったりロールだったりの情報の管理は完全に投げ捨てた)。

基本的な操作は以下のようにして行われる(githubなどでのrepository管理的なものをイメージして作ってみた)。

# サービス上に存在するドキュメントの情報を手元のファイルに同期
$ shosai docbase pull docs/xxx.md

# 手元の変更をサービス上のドキュメントに反映
$ shosai docbase push docs/xxx.md

特定のドキュメントに対してpull/pushを繰り返えすというのが日常になる。誰かがweb上で変更していたのならpullで手元のファイルを更新すれば良いし。手元のファイルをアップデートしたらpushでサービス上のドキュメントに反映すれば良い。文章をリポジトリ上で管理することもできるようになる(githubmarkdownとかであげておけばそれで終わりじゃんみたいな話もあるとは思うのだけれど。例えばプライベートリポジトリで全ての人が見える権限に無いみたいなことがまれにある)。

便利な点

便利な点は、手元のドキュメントが参照している画像も同期される点。けっこうまじめにmarkdownの中から![text](src)部分を取り出してアップロード後に置換している。今までずっと画像つきの記事を書くことから逃げていたところがあったのだけれど、まじめに画像に対応することにした(副産物としてjupyter-nbconvertとの組み合わせてjupyter notebookをしれっとそのまま記事としてアップロードできるようになった。詳しくはappendixを参照)。

手元のファイルがバウンダリーオブジェクト

コマンドの体系(?)として、何を変更したいかの「何を」にあたる部分をファイルによって与える。これがバウンダリーオブジェクトとして使うと言っている部分の意味。ローカルの環境とサービス上の外部の環境は手元のファイルを元に繋げられるみたいなイメージ。

そのため基本的には常に手元にファイルがあることを前提としている。手元にファイルがなければ何もできない。

初回のアップロードもしくはダウンロード

手元にファイルが無いと何もできないと言いつつ、初回は手元にファイルが無いことがある。このようなときにはダウンロードしてくる必要がある。

# docs/xxx.md に保存される
$ shosai docbase clone https://<teamname>.docbase.io/posts/xxxxxx --name xxx.md

名前はオプションで渡さなければid名になる(xxxxxxの部分)。

あるいは、初めて手元で書いたドキュメントはアップロードされていない。その場合はどうなるかと言うと常に下書きでアップロードされる(もちろん、既に公開されているドキュメントを下書きに戻すというようなことはしない)。

# 下書きでアップロード
$ shosai docbase push docs/yyy.md
# 下書きでアップロード(更新)
$ shosai docbase push docs/yyy.md
# 下書きでアップロード(更新)
$ shosai docbase push docs/yyy.md

--publishオプションで公開にもできるが、基本的にはweb ui上で公開する想定(権限設定などはweb uiでやることになるので)。タグ的なものに関しては特別にタイトル部分で設定できるようにした。

# [memo][python] hello

hello world

先頭のh1の[]でくくられた部分がタグとして扱われる。

全体的な流れ

全体な流れをまとめるとこういう形。

# ドキュメントを取ってくる
$ shosai docbase clone https://<> --name=x
# 手元で更新したドキュメントを反映
$ shosai docbase push docs/x.md
# 誰かの更新結果を手元に同期
$ shosai docbase pull docs/x.md
# 再度変更したドキュメントを反映
$ shosai docbase push docs/x.md

appendix: jupyter notebookをアップロード

副産物としてjupyter notebookをしれっとそのまま記事としてアップロードできるようになった。 jupyterのnbconvertを使うとmarkdownが生成できる。これと組み合わせて以下の様な感じで使うことでipynb(juputer notebook)を記事として共有できる。

$ https://raw.githubusercontent.com/jrjohansson/scientific-python-lectures/master/Lecture-4-Matplotlib.ipynb
$ jupyter-nbconvert --to markdown Lecture-4-Matplotlib.ipynb
$ shosai hatena push Lecture-4-Matplotlib.md

たとえばA gallery of interesting Jupyter Notebooks · jupyter/jupyter Wikiこのnotebookこういう感じになる(このサンプルを載せるために部分的にはてなブログに対応させた)。

(テキトーなipynbのサンプルを選んで下書きとして保存という感じにしてしまったけれど。何かダメだったら後で自作のものに置き換える)