railsで特定のタグを通すHTMLエスケープ

掲示板みたいなシステムを作ろうとする時、よく使いそうな処理だけど、
ネットで探してみても見つけられなかったので自分で作ってみた。


普段呼び出す h メソッドを拡張して h(string, :except=>["a", "img"]) のように呼び出せるようにする。
そして、できたのが以下。

def html_escape_extend(str, option = {:except => []})
  if option[:except].empty?
    html_escape(str)
  else
    allows = option[:except].map{|t|t.downcase}
    str.gsub(%r!(</?)([a-z]+)([^>]*)(/?>)!mi){|tag|
      left, name, attr, right = Regexp.last_match.captures
      if allows.include? name.downcase
        attr = attr2hash(attr)
        if tag !~ /^<\//
          attrs = (ATTR_BUILDER[name.downcase]||ATTR_BUILDER[:default]).call attr
          "#{left}#{name} #{attrs}#{right}"
        else
          "#{left}#{name}#{right}"
        end
      else
        html_escape(tag)
      end
    }
  end
end
  
def attr2hash(str)
  str.scan(/([^\s]+?)=(?:'(.+?)'|"(.+?)"|([^ >]+))/i).
      inject({}) {|m, v| m[v[0].intern] = v[1]||v[2]||v[3]; m }
end

ATTR_BUILDER = {
  "a" => lambda{|attr| 
    if attr[:href] =~ /^https?:/
      attrs = [:class, :target].
        map{|key| "#{key}='#{attr[key]}'" if attr.has_key?(key) }.compact.join(" ")
      "href='#{attr[:href]}' #{attrs}"
    end
  },
  "img" => lambda{|attr|
    attrs = attr.map{|key, value| "#{key}='#{value}'" }.join(" ")
    "<img #{attrs} />"
  },
  :default => lambda{|attr|
    [:class, :style].map{|key| 
        "#{key}='#{attr[key]}'" if attr.has_key?(key) }.compact.join(" ")
  }
}

alias h html_escape_extend

html_escape_extendが関数本体で第2引数にexceptオプションとして、
通したいタグ名をリストで渡してやれば、渡されたタグ以外全てHTMLエスケープをして返してくれる。
上記をApplicationHelperにでも書いてやれば既存の処理に影響なく拡張できる。


処理的には単純でタグ部分を正規表現で引っ張って、タグ名が引数exceptに含まれなければ通常のhtml_escape関数に引き渡し、含まれるならタグとして返す。


この時、タグの種類毎に属性に制限もかけてやりたいので、
ATTR_BUILDERで定義されたlambdaを使って許可する属性を構築している。
とりあえずaタグの場合はhref,class,target属性を許し、imgタグは全ての属性、
それ以外はclassとstyle属性のみ許可する設定になっている。


option引数をhash形式にしたのは、今後拡張として指定したタグのみ許可しないonlyオプションを実装したり、truncateも同時に行えるようtruncateオプションなども追加できると面白いと思ったので。