RubyでJSのURIエンコードを

先にまとめ

JavaScript の encodeURIComponent と encodeURI と同じエンコードするメソッドを String クラスに作るモンキーパッチ。

class String
  def encodeURIComponent
    unescaped_form = /([#{Regexp.escape(';/?:@&=+$,<>#%"{}|\\^[]`' + (0x0..0x1f).map{|c| c.chr }.join + "\x7f" + (0x80..0xff).map{|c| c.chr }.join)}])/n
    self.force_encoding("ASCII-8BIT").gsub(unescaped_form){ "%%%02X" % $1.ord } 
  end
  
  def encodeURI
    unescaped = /([#{Regexp.escape('<>#%"{}|\\^[]`' + (0x0..0x1f).map{|c| c.chr }.join + "\x7f" + (0x80..0xff).map{|c| c.chr }.join)}])/n
    self.force_encoding("ASCII-8BIT").gsub(unescaped){ "%%%02X" % $1.ord } 
  end
end

encodeURIComponent() と encodeURI() と同じく、文字列を引数を取るメソッドはこうなります。

def encodeURIComponent(str)
  unescaped_form = /([#{Regexp.escape(';/?:@&=+$,<>#%"{}|\\^[]`' + (0x0..0x1f).map{|c| c.chr }.join + "\x7f" + (0x80..0xff).map{|c| c.chr }.join)}])/n
  str.force_encoding("ASCII-8BIT").gsub(unescaped_form){ "%%%02X" % $1.ord } 
end

def encodeURI(str)
  unescaped = /([#{Regexp.escape('<>#%"{}|\\^[]`' + (0x0..0x1f).map{|c| c.chr }.join + "\x7f" + (0x80..0xff).map{|c| c.chr }.join)}])/n
  str.force_encoding("ASCII-8BIT").gsub(unescaped){ "%%%02X" % $1.ord } 
end

使用の際の注意

まとめ
◎escape
古くからある関数。ブラウザ・バージョン・HTMLの文字コードによって挙動が変わる。推奨されない。
◎encodeURI
URI全体に適用するための関数。URIの予約文字をエンコードしないため、不完全なエンコードとなる。
◎encodeURIComponent
URIを構成する部分文字列に適用するための関数。本来のエンコード目的であればこれを使うべき。URI全体にかけてしまうと無効なURIになる。

エンコードが必要な部分ごとにencodeURIComponentをかけた上で結合しURIを完成させる」というのがあるべき姿だと思います。
escape()とencodeURI()とencodeURIComponent()の違い - Miuran Business Systems

経緯など

以前こういう記事を書きました。

ERB::Util.url_encode
   ┆   CGI.escape
   ┆     ┆   URI.encode_www_form_component
   ┆     ┆     ┆   WEBrick::HTTPUtils.escape_form
   ┆     ┆     ┆     ┆   WEBrick::HTTPUtils.escape
   ┆     ┆     ┆     ┆     ┆   URI.escape
   ┆     ┆     ┆     ┆     ┆     ┆
["%20", "+",   "+",   "+",   "%20", "%20"]
["%21", "%21", "%21", "!",   "!",   "!"]
["%24", "%24", "%24", "%24", "$",   "$"]
["%26", "%26", "%26", "%26", "&",   "&"]
["%27", "%27", "%27", "'",   "'",   "'"]
["%28", "%28", "%28", "(",   "(",   "("]
["%29", "%29", "%29", ")",   ")",   ")"]
["%2A", "%2A", "*",   "*",   "*",   "*"]
["%2B", "%2B", "%2B", "%2B", "+",   "+"]
["%2C", "%2C", "%2C", "%2C", ",",   ","]
["%2F", "%2F", "%2F", "%2F", "/",   "/"]
["%3A", "%3A", "%3A", "%3A", ":",   ":"]
["%3B", "%3B", "%3B", "%3B", ";",   ";"]
["%3D", "%3D", "%3D", "%3D", "=",   "="]
["%3F", "%3F", "%3F", "%3F", "?",   "?"]
["%40", "%40", "%40", "%40", "@",   "@"]
["%5B", "%5B", "%5B", "%5B", "%5B", "["]
["%5D", "%5D", "%5D", "%5D", "%5D", "]"]
["%7E", "%7E", "%7E", "~",   "~",   "~"]

URLエンコード/エスケープに使うメソッドごとの違い - 別館 子子子子子子(ねこのここねこ)

これを書いたときにはまだ JavaScript に興味がさほどなく、単に URL エンコードしたかっただけでした。

encodeURIComponentを改めて確認

英数字以外のASCII文字をどうエンコードするのか確認してみます。

> console.log(encodeURIComponent(" !\"#$%&\'")+"\n"+
  encodeURIComponent("()*+,-./")+"\n"+
  encodeURIComponent(":;<=>?@")+"\n"+
  encodeURIComponent("[\\]^_`{|}~"));

%20!%22%23%24%25%26'
()*%2B%2C-.%2F
%3A%3B%3C%3D%3E%3F%40
%5B%5C%5D%5E_%60%7B%7C%7D~ 

Rubyのメソッドで近そうなのはWEBrick::HTTPUtils.escape_form

> require 'webrick/httputils'
> puts WEBrick::HTTPUtils.escape_form(" !\"#$%&\'") + "\n" + 
* WEBrick::HTTPUtils.escape_form("()*+,-./") + "\n" + 
* WEBrick::HTTPUtils.escape_form(":;<=>?@") + "\n" + 
* WEBrick::HTTPUtils.escape_form("[\\]^_`{|}~")

+!%22%23%24%25%26'
()*%2B%2C-.%2F
%3A%3B%3C%3D%3E%3F%40
%5B%5C%5D%5E_%60%7B%7C%7D~

ほぼ同じなのですが、唯一の違いが半角スペースのエンコード
encodeURIComponent だと %20 ですが、 WEBrick::HTTPUtils.escape_form だと + (プラス記号)になります。

+を%20に置換すればencodeURIComponentと等しい

ということは + を %20 に置換してやればOKですね。

> puts WEBrick::HTTPUtils.escape_form(" !\"#$%&\'").gsub("+", "%20")
%20!%22%23%24%25%26'

モンキーパッチしちゃいましょう

class String
  require 'webrick/httputils'
  def encodeURIComponent
    WEBrick::HTTPUtils.escape_form(self).gsub("+", "%20")
  end
end

WEBrick::HTTPUtils.escape_formを確認

module WEBrick
  module HTTPUtils
    reserved = ';/?:@&=+$,'

    control  = (0x0..0x1f).collect{|c| c.chr }.join + "\x7f"
    delims   = '<>#%"'
    unwise   = '{}|\\^[]`'
    nonascii = (0x80..0xff).collect{|c| c.chr }.join

    def _make_regex(str) /([#{Regexp.escape(str)}])/n end

    UNESCAPED_FORM = _make_regex(reserved+control+delims+unwise+nonascii)

    def _escape(str, regex) str.gsub(regex){ "%%%02X" % $1.ord } end

    def escape_form(str)
      ret = _escape(str, UNESCAPED_FORM)
      ret.gsub!(/ /, "+")
      ret
    end
  end
end

これを使って encodeURIComponent に合致する URL エンコードを作ることも出来ますね。

モンキーパッチしちゃいましょう2

class String
  def encodeURIComponent
    unescaped_form = /([#{Regexp.escape(';/?:@&=+$,<>#%"{}|\\^[]`' + (0x0..0x1f).map{|c| c.chr }.join + "\x7f" + (0x80..0xff).map{|c| c.chr }.join)}])/n
    self.force_encoding("ASCII-8BIT").gsub(unescaped_form){ "%%%02X" % $1.ord } 
  end
end

encodeURIだと

> console.log(encodeURI(" !\"#$%&\'")+"\n"+
  encodeURI("()*+,-./")+"\n"+
  encodeURI(":;<=>?@")+"\n"+
  encodeURI("[\\]^_`{|}~"));

%20!%22#$%25&'
()*+,-./
:;%3C=%3E?@
%5B%5C%5D%5E_%60%7B%7C%7D~

Rubyのメソッドで近そうなのはWEBrick::HTTPUtils.escape

> puts WEBrick::HTTPUtils.escape(" !\"#$%&\'") + "\n" + 
* WEBrick::HTTPUtils.escape("()*+,-./") + "\n" + 
* WEBrick::HTTPUtils.escape(":;<=>?@") + "\n" + 
* WEBrick::HTTPUtils.escape("[\\]^_`{|}~")

%20!%22%23$%25&'
()*+,-./
:;%3C=%3E?@
%5B%5C%5D%5E_%60%7B%7C%7D~

ほぼ同じなのですが、唯一の違いがシャープ # のエンコード
encodeURI だと # のままですが、 WEBrick::HTTPUtils.escape だと %23 になります。

%23を#に置換すればencodeURIと等しい

> puts WEBrick::HTTPUtils.escape(" !\"#$%&\'").gsub("%23", "#")
%20!%22#$%25&'

モンキーパッチしちゃいましょう3

class String
  require 'webrick/httputils'
  def encodeURI
    WEBrick::HTTPUtils.escape(self).gsub("%23", "#")
  end
end

require 'webrick/httputils' がウザイですな。

モンキーパッチしちゃいましょう4

class String
  def encodeURI
    unescaped = /([#{Regexp.escape('<>#%"{}|\\^[]`' + (0x0..0x1f).map{|c| c.chr }.join + "\x7f" + (0x80..0xff).map{|c| c.chr }.join)}])/n
    self.force_encoding("ASCII-8BIT").gsub(unescaped){ "%%%02X" % $1.ord } 
  end
end