Chienomi

PureBuilder Simply 3.0 Released!

開発::util

ちょうどPureBuilder Simply用の新しいドメインを取得した翌日にissueが立ったのを機に、PureBuilder Simply 3.0の開発に着手し、2日ほどかけてリリースにこぎつけた。

PureBuilder Simply 3.0は1.15から2.0になったとき以上に大きなコード的変更が加わっている。 ただし、基本的にバグではない互換性は破壊されていない――ただし、プラグイン機能は非推奨になった。

2022年の頭にv2.0がリリースされてから、すぐに私はv2.1の開発に取り掛かった。 そして、実際にv2.1は2月には完成していたのだが、そのコンセプトが正しいのかどうか自信が持てなかった。 その結果、developブランチで塩漬けにされた。

その後、2023年になってからv2.2が誕生した。これは2.1のときには中途半端だったhooks機能を実装したものだ。 v2.2はChienomiをビルドするために使われたが、結局リリースには至らなかった。

v3.0はv2.1やv2.2で行われた変更を焼き直して投入している。

リファクタリング

v3.0において最も重要だったのがリファクタリングだ。

PureBuilder Simplyはv2.2の時点で1000行ほどのコードになっていた。もはや、決して小さなコードではない。

Code OSSではRubyのシンボルをうまく扱えないという問題がある。サポートを強化するRubyの拡張もどれも停滞気味だ――開発されたライブラリが放置されやすいのはRuby圏の悪い文化だ。

このため、大きな見通しの悪いRubyコードは、少なくともCode OSSで開発をしているとかなり扱いにくい。 Node.jsのコードならF12で解決できるが、それもできないのだ。

そこで、私はPureBuilder Simplyのコードを細かく分割することを決めた。

もちろん、現在においてはRubyは機能を細かく分割しないことを推奨していることは知っている――JITコンパイラの効率が下がるからだ。 しかし、PureBuilder Simplyは繰り返し呼ばれるメソッドはあまりなく、そもそも私ひとりで開発しているものだから実効効率よりも開発効率のほうが何倍も大事である。

PureBuilder Simplyは基本的にはあるフローに従って処理するプログラムである。 つまり、「段階的な進行」というものが存在する。

また、PureBuilder Simplyの機能はまとまった概念が存在している。 例えばFrontmatter, BLESS, Plguins, Processのようにだ。

そこで、私は「処理の段階」をメソッドに分け、概念のかたまりをファイルに分けることにした。

Rubyは、複数ファイルへの分割は、苦手ではないが得意でもない。 従来はただのメソッドに過ぎなかったようなものを分割するには、主に3つの手法がある。

1つ目は、モジュールとして書き、メインのクラスにMix-inすることである。 これはメソッドを複数に分割して書けるから、シンプルだ。

2つ目は、別のクラスにすることである。 インスタンス変数など、一部の情報にはアクセスできなくなるから、コードの書き直しが少し必要になる。 また、きれいに分割する設計も必要だ。

3つ目は、単一のクラスを分割して書くことである。 問題はないが、汚い。

幸いにも過去の私が綺麗に書くよう心がけていてくれたので、量は多かったが、それほど苦労することはなかった。

この作業は、複雑になったPureBuilder Simplyの流れと概念を整理するのにも役立った。 結果的には今まで私が積み上げてきたものは間違ってはいなかったが、見落としによるバグは生じていた。

また、このリファクタリングにおいてコアとなる作業がふたつあった。

proc_docs

ひとつは、新しく導入されたメソッドである#proc_docsである。

もともとの処理の流れとしては、#main#proc_dirを呼び、#proc_dirが処理の前提を用意するところから出力するところまでを担っていた。

ところが、PureBuilder Simplyはディレクトリだけではなく、ファイル単位で処理することもできる。 v2.2ではファイル更新時もACCSインデックスの更新が行われるようになり、ACCSディレクトリでも単一ファイルを処理できるようになった。

しかし、ファイル単位の処理は当然ながら#proc_dirが呼ばれることはなく、この場合の処理は(#proc_dirでしているのと同じようなことを)#mainの中でやっていた。

ここで処理を追加したり改善した場合に、single modeには反映されていない、あるいはsingle modeでバグがあるといった問題が継続的に発生していた。

リファクタリングによってdir modeとsingle modeを同じメソッドで処理するようにする、というのが最も重要なポイントだった。これは、issueの解消にもなる。

とはいえ、ディレクトリの全ファイルを処理するのと、単一ファイルを処理するのでは話が違う。 単一ファイルを処理するために、ディレクトリの他のファイルを見るのも筋が違うというものだ。

そこをうまく吸収すべく書かれたのが、新しい#proc_docsである。 #proc_docsは対象となるドキュメント群を指定するが、single modeでは長さ1の配列を渡す形になっている。

Hooks

もうひとつは、Hooksの再実装である。 v2.1ではHooksは特定の機能を有効にするかどうかというだけのものだった。 v2.2ではハッシュにProcをセットしておくことで機能するものになった。

v3.0ではこの実装を捨て、PBSimply::Hooksという新しいクラスに分離された。

PBsimply::Hooksは「タイミングオブジェクト」というのを持っている。 その実態は、インスタンス変数に対するアクセサメソッドであり、インスタンス変数はPBSimply::Hooks::HooksHolderオブジェクトである。

HooksHolderオブジェクトは、配列に対する#<<と、配列を#eachして#callする#runメソッドを持っている。 まぁ、極めて単純なものだ。

# Timing object class.
class HooksHolder
  def initialize name
    @name = name
    @hooks = []
  end

  def <<(proc)
    @hooks << proc
  end

  alias  :<<

  def run(arg)
    STDERR.puts "Hooks processing (#{@name})"
    @hooks.each_with_index do |proc, index|
      STDERR.puts "Hooks[#{index}]"
      begin
        proc.(arg)
      rescue
        STDERR.puts "*** HOOKS PROC ERROR ***"
        raise
      end
    end
  end
end

おかげでだいぶ単純になった。

def (PBSimply::Hooks).load_hooks h
  h.process << ->(v) {
    db[v["normalized_docpath"]] = v
  }

  h.post << ->(v) {
    db.delete_if do |dbk, dbv|
      not File.exist? dbv["dest_path"]
    end
  }
end

削除

これとの兼ね合いなのだが、今まで「削除された記事」の扱いがふわっとしていて、半分バグ状態だった。

v2.2では「記事がdraftになった」ということを検出すると出力ドキュメントも消すようになり、「記事公開の取り消し」ができるようになったが、これも私の都合でしかない変更だ。

v3.0では、「draftになった」場合と「記事のソースファイルが消えた」場合の両方でdeleteがかかるようになり、インデックスから削除されるようになった。 デフォルトではインデックスから消すだけで、設定でauto_deleteを有効にしている場合のみ出力ファイルも削除する。

さらに、Hooksのdeleteタイミングが使えるようになったため、削除時にそれ以外の処理を行いたい場合も有効。 ただし、deleteで渡されるのはソースファイルの(予期される)パスと出力ファイルの(予期される)パスだけなので、正しく扱いたい場合はもうちょっと工夫が必要。 どちらかというと、全記事を処理し終わった後に呼ばれるpostのほうが使いやすいかもしれない。

プラグイン

プラグイン機能は今回リファクタリングでちゃんと別ファイルになったのだが、同時にdeprecatedになった。

この理由は、プラグインで使用している仕組みが、PureBuilder Simplyとしては古い挙動だからだ。 pre pluginsはPandocのソースをいじるためのものだし、post pluginsは出力されたHTMLをいじるもので、現在のPureBuilder Simplyからすると結構な力技である。

なのでできればHooksを使ってほしいのだが、単に廃止してしまうと困ると思うので、あくまでドキュメントから消えただけである。

また、Rubyを書かない人が困るという面もあると思うので、v3.1ではコマンドを使うhooksをサポートするつもりだ。 例えば、フィルタコマンド、つまり標準入力に現在のソースドキュメントを渡し、標準出力の結果を反映して欲しい場合はこんな感じ。

h.process.filter("foo", "-a")

同じことをfrontmatterでしたい場合はこう

h.process.frontmatter_filter("foo", "-b")

標準入出力を使わず、環境変数を参照して動作する比較的新しいpluginsと同じ挙動にしたい場合はこう。

h.process.command("bar", "-c", "-x")

PureBuilder Simplyのカレントディレクトリはドキュメントルートに制約されているので、書くのは難しくないと思う。

ただ、こういうことができるhooksは限られるため、タイミングに固有のものになる。 このことから、もう少しちゃんとした設計が必要で、v3.0に併せて入れてしまうとややこしいので、今回の導入は見送った。

auto blessの追加

PureBuilder SimplyにはBLESSする前にPureBuilder Simply側でfrontmatterを拡張するauto blessというものがある。

ここで追加されるパラメータにdest_path, normalized_docdir, normalized_docpathが追加された。

このパスの正規化はバグ取りでもある。 というのも、pbsimply docとするのと、pbsimple ./docとするのでパス表現が変わるため、インデックスが更新されたように見えるという問題があったのだ。

この正規化は、カレントディレクトリがドキュメントルートであるという制約を利用して絶対パスを使うことで正規化している。 これですべてのケース正しく動作するか(特にシンボリックリンクがらみ)は確認できていない。

また、この機能のために最低限要求されるRubyのバージョンが2.6になった。 別に古いバージョンで動くようにも書けるのだが、いつまでも新しい書き方を避けるのは変な話なので。

もし、「frontmatterにこういうパラメータを持つようにしてほしい」という要望があれば、issueを立ててもらえれば、一般化できそうなら採用するので、遠慮なく寄せてほしい。

その他バグ修正

  • 環境変数 $pbsimply_indexes$pbsimply_frontmatterが渡っていなかった
  • Ruby 3.0以降の環境でdbstyleYAMLを指定すると、Dateが読めなくて例外を発生していた

という問題を修正した。

現状やりたいこと

まず3.0.1として実施予定なのが、更新チェック方法を指定できるようにすることである。 3.0で更新チェックの方法が変わったのだが、これだとblessの書き方次第で更新されていないという判定ができない。

3.0.1リリース後に検討していることは:

  • 非Pandocのドキュメントエンジンをもうちょっと支援できないか考えている
  • Rubyを得意としない人にもhooksを書きやすくしたい
  • PureBuilder Simplyの公式サイトを作りたい (ドメインは取った)
  • 出力フォーマットJSONの追加
  • プロジェクト初期化コマンドの追加

JSON出力は、jsonプロセッサの追加(documentはプロセッサを通さない)に加えて、プロセッサ+JSONも入れたい。 前者はともかく、後者はちょっと大変。

かなり熟成してきていて、私自身ウェブサイト構築にもう6年使ってるわけで、そろそろ普及に向けてもっと力を入れていきたい。 よりわかりやすい解説も必要だろう。

PureBuilder Simplyの立ち位置

JekillとかNuxtとかは、プログラマ寄りというか、フレームワークが提供されるので、それに従って(事実上プログラムを)書くという仕組みになっている。

もっと簡単なSSGもあるにはあるけど、ウェブサービスとして展開されていて、結局やってることはWordPressと変わらない(生成タイミングの話でしかない)という感じ。

PureBuilder Simplyはその中間、かつもっと柔軟なものを指向している。

ユーザーは、

  • HTML, CSSがちょっとだけわかる
  • 言われた通りコマンド打つくらいはできる
  • Markdownは、段落くらいは書ける

くらいのレベル感を最低ラインとする感じで、基本的に「サイトを構築すること」よりも「文章を書くこと」にフォーカスしたい。

最近はSSG割と流行なので色々出てきてるけど、PureBuilder Simplyと同じような立ち位置のものはなさそう。 エンジンにPandoc使っているのも強い。

広げていく部分は私ひとりでやっていくのは厳しすぎるから、多くの人に広げていってほしいね。