2018-12-08

My first Jekyll GeneratorのためにJekyllを読む

先日Jekyll と Middleman の比較をして、Jekyll の方が拡張はやりやすそうという結論を出したわけだけど、実際に書くとなったら公式のドキュメントを読んでるだけだとピンとこない部分があって、それが Generator.

Generators | Jekyll • Simple, blog-aware, static sites

いきなり Generator に site オブジェクトを突っ込んで、そこから Page を生成する例が出てくるんだけど、なんでこれでうまくいくのかがよく分からない。

この理由はそもそも全体の流れが説明されていないから。ということでコードを読んでみることにする。

対象バージョンは現行の Jekyll 3.8.5

まずはcommandから

jekyll の操作は

$ jekyll <command>

から始まる。これを担うのが commands/ 以下のコマンド群だ。

exe/jekyll

を読むと command 群は init_with_program が呼ばれるのが分かるので、例えば build.rb で追っていくと

        def init_with_program(prog)
          prog.command(:build) do |c|
            c.syntax      "build [options]"
            c.description "Build your site"
            c.alias :b

            add_build_options(c)

            c.action do |_, options|
              options["serving"] = false
              Jekyll::Commands::Build.process(options)
            end
          end
        end

中で process を呼び、process では

        def process(options)
..
          site = Jekyll::Site.new(options)
..
            build(site, options)

build を呼んでて、

        def build(site, options)
..
          process_site(site)

process_site は superclass の

      def process_site(site)
        site.process

で、Jekyll::Site オブジェクトにたどりつく。

すべてを司るJekyll::SiteオブジェクトからGenerator#generateが呼ばれる

Jekyll::Site#process はこれだけ。

    def process
      reset
      read
      generate
      render
      cleanup
      write
      print_stats if config["profile"]
    end

これを見るとピンとくるのが

Hooks | Jekyll • Simple, blog-aware, static sites

ですね。ここでようやく全体の流れが分かる。これを見ると Generator は Reader と Renderer の間で仕事をしていることが分かる。

generate の中身は

    def generate
      generators.each do |generator|
        start = Time.now
        generator.generate(self)
        Jekyll.logger.debug "Generating:",
          "#{generator.class} finished in #{Time.now - start} seconds."
      end
    end

登録された Generator それぞれの generate メソッドに Jekyll::Site そのものが渡され、site を書き換えることでコンテンツを増やし、その後 render, write が仕事をすることで実際のファイルが生成される、という流れになる。

なるほど。

Generator間の依存関係はpriorityで調整

generator の初期化は site.rb の #setup の中の

      self.generators = instantiate_subclasses(Jekyll::Generator)

からの

    def instantiate_subclasses(klass)
      klass.descendants.select { |c| !safe || c.safe }.sort.map do |c|
        c.new(config)
      end
    end

で、ここの sort で plugin.rb の

   def self.<=>(other)
     PRIORITIES[other.priority] <=> PRIORITIES[self.priority]
   end

が呼ばれて、priority の高いものから並ぶようになっている。

※ ちなみにこの setup が実行され終わる時に after_init に register した hook が走るようになっている。

したがって、例えばカテゴリやタグといった情報をもとにイマドキのリッチなスライドっぽいリンクを並べた一覧ページを作りたいということになったら、

  1. Page をなめて content から data ( Front Matter ) の中に最初の heading や paragraph, img など、リンク生成時に使いたいデータをまとめ直す処理を行う
  2. 1 のデータをもとにカテゴリやタグの Page を追加する

この際に 1, 2 の順になるように priority を調整する、という感じで利用することができる。

※ これくらいならひとまとめでもよさそうだけど、他の generator で生成したコンテンツも利用できるようになっていた方が都合がよいかもしれない。分かってくると Ganerator は応用範囲が広い感じがする。

MiddlemanのproxyのようなものもGeneratorで解決できる

考え方は以下のような感じ。

  1. site.data, site.pages などから必要なデータを取得して
  2. 取得したデータをもとに Jekyll::PageWithoutAFile を継承した Page を生成
    • この際、Page#initialize とはリスコフの置換原則を破るようにしないと page 独自の data は渡せないことに注意が必要。
   def initialize(site, base, dir, name)

適当に keyword args 足せばいいと思う :p

layout はいちいち指定してもいいけど、_config.yml の defaults で一気に指定するようにした方がたぶん他のものと整合性が確保できてよい感じになる。

Front Matter Defaults | Jekyll • Simple, blog-aware, static sites

About

例によって個人のなんちゃらです