こんにちは、hachi8833です。
ここでは次のような結果を期待していました。以下コード中の&は、エスケープを表示するために全角の&にしています。
<%= link_to '#', data: {key: '<span>piyopiyo</span>'} %> <!-- 元のERBコード -->
<a href="#" data-key="&lt;span&gt;piyopiyo&lt;/span\&gt;"> <!-- 期待するHTML出力 -->
<a href="#" data-key="<span>piyopiyo</span>"> <!-- 実際のHTML出力 -->
Railsビューのエスケープ系操作は、#html_safeメソッドの名前が微妙に紛らわしいせいかときどき迷うことがあるので、まず整理してみました。
Railsのビューにおけるエスケープ
一般的なHTMLエスケープについてまずまとめました。Railsガイドもどうぞ。
参考: §5.1.2 安全な文字列 -- Active Support コア拡張機能 - Railsガイド
エスケープした場合としなかった場合
コントローラとビューにそれぞれ以下のように書いたとします。
# コントローラ
@h1 = "<h1>Railsドキュメント</h1>"
<!-- ビューのERB -->
<%= @h1 %>
@h1変数がエスケープされれば以下が出力され、
<!-- ビューのERBから出力されるHTML -->
&lt;h1&gt;Railsドキュメント&lt;/h1&gt;
ブラウザでタグとして解釈されずに以下のように表示されます。上のSlackのやりとりではこのような出力を期待していました。
@h1変数がエスケープされなければ以下が出力され、
<!-- ビューのERBから出力されるHTML -->
<h1>Railsドキュメント</h1>
ブラウザでh1タグとして解釈されてたとえば以下のように表示されます。
ビューの文字列はデフォルトでエスケープされる
安全のため、Rails 3 以降のビューで表示される変数はデフォルトですべてエスケープされます。必要な場合にのみ、このエスケープを解除することになります。
HTML出力を
h(string)呼び出しでエスケープする必要はもうありません。h(string)はデフォルトであらゆるビューテンプレートで有効になります。エスケープを解除した(unescaped)文字列が欲しい場合はraw(string)を呼び出します。
§7.4.3 その他の変更 -- Ruby on Rails 3.0 リリースノート - Railsガイドより
Rails 2 以前の案件を扱う場合、マニュアルでのエスケープ処理が不完全だったり、まったく行われていない可能性があるので注意が必要です。
ERBやhamlでのエスケープ解除
ERB
ERB で<%==と%>で記述したコードの出力は、エスケープされなくなります。
<%== エスケープされないRubyコード %>
参考: 結果をエスケープしないで出力 -- railsdoc.com
haml
hamlの場合は=の代わりに!=を使います。
= "I feel <strong>!" <!-- エスケープされる -->
!= "I feel <strong>!" <!-- エスケープされない -->
参考: Unescaping HTML: != -- haml.info
個人的にはこれらの形式のエスケープ表記は見落としそうなので使っていません。
#html_escapeまたは#h
#html_escapeはRubyのERB::Utilのメソッドであり、#hはそのエイリアスです。前述のとおり、Rails 3 以降は何もしなくてもデフォルトでエスケープされるので、通常のエスケープのためにこのメソッドを呼ぶ必要はありません。
参考: Rails API html_escape -- ERB::Util
参考: Rails API h -- ERB::Util
#html_safe
#html_safeは、対象の文字列が安全であるとマーキングするメソッドです。
<!-- ビューのERB -->
<%= @h1.html_safe %>
参考: Rails API String#html_safe
安全であるとマーキングすることで、対象の文字列では以後の処理でエスケープされなくなります。
次の#rawと同等のメソッドです。
#html_safeや#rawではなく、後述の#sanitizeメソッドを使うことが推奨されています。セキュリティ上の問題が生じるため、ユーザー入力に対して#html_safeや#rawを使ってはいけません。
It is recommended that you use
sanitizeinstead of this method. It should never be called on user input.
Rails APIString#html_safeより
個人的には、#mark_as_safeというメソッド名にして欲しかった気もします。#html_safe?と一貫させるためなのかもしれませんが。
参考: Rails API html_safe? -- ActiveSupport::SafeBuffer
#raw
#rawは上述のとおり、#html_safeと同等です。
<!-- ビューのERB -->
<%= raw @h1 %>
参考: Rails API raw -- ActionView::Helpers::OutputSafetyHelper
#sanitize
#sanitizeは、tagsやattributesで指定されていないタグや属性をすべて除去します。href属性やsrc属性にjavascript:などの安全でないプロトコルが指定されている場合も削除します。
sanitize(文字列 [, tags => "許可するHTMLタグ名", attributes => "許可するHTML属性名"])
railsdoc.com サニタイズ(sanitize)より
ユーザー入力に#sanitizeを使っても、<、>、&などが残る可能性があるので、安全は保証されません。
参考: Rails API sanitize -- ActionView::Helpers::SanitizeHelper
#link_toではエスケープが解除される
やっと本題に戻ります。baba さんが指摘しているように、ビューの#link_toメソッドはデフォルトのエスケープ対象とならず、#html_safeが適用されたのと同じになります。
#link_toメソッドでこの挙動を解除して通常どおりエスケープされるようにするには、#to_strメソッドを使います。「エスケープ解除を解除」になるので、一瞬考えてしまいました。
<%= link_to("リンク文字", path).to_str %>
#to_sやString()はこの目的には使えません。
追伸
#link_toに限らず、ヘルパーメソッドの多くはエスケープ対象となりません(#button_to、#content_tag、#form_for、#collection_check_boxesなど)。


更新情報