RailsでHTMLをいじるhtml-pipelineを独自実装する方法サンプル

html-pipelineはHTMLをparseするGem

html-pipeline github.com

RailsでHTMLをいじる代表といえば、Nokogiriだと思いますが、今回は複数の「フィルター」を疎結合に使えるhtml-pipelineを採用しました。

なぜhtml-pipelineか

  • 「フィルター」を複数組み合わせることができる
  • 書き方もシンプル
  • 既存実装にあわせた

Nokogiriでもいいのですが、例えば以下のようなケースを考えるとhtml-pipelineの方が拡張性があると判断しました。

ex) * ページAでは、フィルターAのみを使う * ページBでは、フィルターAに加えて、Bも使う * ページCでは、フィルターAは不要で、Bと新たに作成するCを使いたい

みたいなケースの時に、html-pipelineではフィルターA,B,Cを疎結合に作成、定義して、適宜組み合わせて使うことができます。例えばこんな感じで

EmojiPipeline = Pipeline.new [
  PlainTextInputFilter,
  EmojiFilter
], context

独自実装する場合

html-pipeline github.com

曰く、

require 'uri'

class RootRelativeFilter < HTML::Pipeline::Filter

  def call
    doc.search("img").each do |img|
      next if img['src'].nil?
      src = img['src'].strip
      if src.start_with? '/'
        img["src"] = URI.join(context[:base_url], src).to_s
      end
    end
    doc
  end

end

うん、わからん

解読するに、大事なことはこれだけらしい

  • HTML::Pipeline::Filter を継承した独自classを定義すること
  • def call で始めること
  • 呼ぶときは pipeline.call(hoge) のような感じ

実際のサンプルがこちら

class Html::Pipeline::LazyloadImageFilter < HTML::Pipeline::Filter
  def call
    doc.search('img').each do |image_node|
      next if image_node['src'].nil?

      image_node['data-src'] = image_node.attribute('src').value
      image_node['src'] = ActionController::Base.helpers.asset_path('lazy-dummy-rectangle.jpg')
      image_node['class'] = 'lozad lazyload lazyload--wide'
    end

    doc
  end
end

この中で行なっているのは、imgタグの属性やらclassを変換しているだけです。ちなみに、Railsのレールにそって lib/ に置いています。

基本的にNokogiriに似たような感じで使えて、シンプルですね。

続いて使う側がこちら

pipeline = ::HTML::Pipeline.new([
  Html::Pipeline::LazyloadImageFilter
])

hoge = pipeline.call(html_text)[:output].to_s

html_text にはHTML形式のtextが入っていて、それをcallの引数に渡しています。実際に手元で試すとわかるのですが、pipeline.call(html_text)だけだとオブジェクト形式のデータがかえってくるので、その中からoutputだけを抜き出す必要があります。to_sはなくてもいいかも?