たくあんおいしいのブログ

たくあんを食べながらプログラミングとかしますよ

Railsのcontent_tagを読もう

railsバージョンは4.2.5

content_tagの使用例

http://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.htmlのExamplesには

content_tag(:p, "Hello world!")
 # => <p>Hello world!</p>
content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
 # => <div class="strong"><p>Hello world!</p></div>
content_tag(:div, "Hello world!", class: ["strong", "highlight"])
 # => <div class="strong highlight">Hello world!</div>
content_tag("select", options, multiple: true)
 # => <select multiple="multiple">...options...</select>

<%= content_tag :div, class: "strong" do -%>
  Hello world!
<% end -%>
 # => <div class="strong">Hello world!</div>

とある. 第4引数のescapeにfalseを指定するとどうなるのかこの例には載ってないが, 説明にはSet escape to false to disable attribute value escaping.とある.
試した例が以下.

content_tag :p, '<p>hoge</p>', {}, true
# => <p>&lt;p&gt;hoge&lt;/p&gt;</p>

content_tag :p, '<p>hoge</p>', {}, false
# => <p><p>hoge</p></p>

要素内のHTML特殊文字をエスケープするかどうかということらしい. 説明通りだね.

ソースコード

rails/tag_helper.rb at v4.2.5 · rails/rails · GitHubから抜粋

def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
  if block_given?
    options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
    content_tag_string(name, capture(&block), options, escape)
  else
    content_tag_string(name, content_or_options_with_block, options, escape)
  end
end

読もう

1行目

ブロックが渡った場合とそうでない場合で分岐する

2,3行目

ブロックが渡っている場合は, 第2引数が省略されたかのように呼び出されるため, 本来は第3引数optionsに渡されるはずのHashが第2引数に渡ってきてしまう.

# Exampleの'第2引数が省略されたかのように呼び出されている'コード
<%= content_tag :div, class: "strong" do -%>
  Hello world!
<% end -%>

ので, 改めて第3引数optionsに第2引数のHashを渡している. このとき, 第2引数content_or_options_with_blockがHashであるかどうかのチェックが挟まっている.

さらに, 3行目ではcontent_or_options_with_blockは使用されていないので, ブロックを渡してcontent_tagを呼び出した場合は, 第2引数が省略されたかのように呼び出そうが普通に第2引数渡そうが関係ないのだ. 第2引数が無視されるような挙動になる.

はじめcontent_or_options_with_blockという名前を見た時は「長すぎだろ」と思ったが, どうもこのへんの挙動から命名された引数名だったようだ.

あとこういう実装見るとJavaみたくメソッドオーバーロードさせてくれよと思う.

3行目

content_tag_string(name, capture(&block), options, escape)の返り値がcontent_tagの返り値になる.

content_tag_string is 何

コードは以下. rails/tag_helper.rb at v4.2.5 · rails/rails · GitHub より.

def content_tag_string(name, content, options, escape = true)
  tag_options = tag_options(options, escape) if options
  content     = ERB::Util.unwrapped_html_escape(content) if escape
  "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
end

なんかもうめんどくさいので省略するが,

そしてcontent_tag_stringの返り値が

"<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe

になる.

capture(&block) is 何

コードは以下. rails/capture_helper.rb at v4.2.5 · rails/rails · GitHub より.

def capture(*args)
  value = nil
  buffer = with_output_buffer { value = yield(*args) }
  if string = buffer.presence || value and string.is_a?(String)
    ERB::Util.html_escape string
  end
end

ブロックの内容を文字列にして返すメソッドということだけはわかった. 詳しくはまた今度読もう. (もう疲れたゾ)

5行目

ブロックが渡っていない場合はシンプル. 第2引数と第3引数の格納しなおしなんかもないので

content_tag_string(name, content_or_options_with_block, options, escape)

がcontent_tagの返り値になる.

読めた

今度はcaptureとかも読もう.