rubyでhtmlの整形

自作しようかと思ったんですが、CGI::prettyというのが
既にあるということでソースを眺めてみました。

ソース

1.8#lib/ruby/1.8/cgi.rb
1.9#lib/ruby/1.9.1/cgi/util.rb

  def CGI::pretty(string, shift = "  ")
    lines = string.gsub(/(?!\A)<(?:.|\n)*?>/n, "\n\\0").gsub(/<(?:.|\n)*?>(?!\n)/n, "\\0\n")
    end_pos = 0
    while end_pos = lines.index(/^<\/(\w+)/n, end_pos)
      element = $1.dup
      start_pos = lines.rindex(/^\s*<#{element}/ni, end_pos)
      lines[start_pos ... end_pos] = "__" + lines[start_pos ... end_pos].gsub(/\n(?!\z)/n, "\n" + shift) + "__"
    end
    lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/n, '\1')
  end


すごい、こんだけとは。自作するとバグ込みで3倍くらいになりそう。
第2引数に、好きなインデントを設定できるようです。
パッと見訳分かんないので、上から追ってみます。


まずはおさらい

こちらをガン見。
正規表現 - Rubyリファレンスマニュアル


(?= ) 先読み
(?! ) 否定先読み
\A   文字列先頭。^ とは異なり改行の有無には影響しません。
/n   オプションのnは知らなかったけど、文字コードnoneという意味らしい。
(?: ) 項参照を伴わないグループ化

最初にまとめ

・1行目は、開始・終了タグの前後に改行を挿入。
・ループは、開始・終了タグを一塊として、前後に目印「__」を挿入し、
 その塊の中にある改行の後にインデント(空白2個)を挿入することで階層化しています。
・最後に目印「__」を削除。


あぁ、ややこしぃ。

では1行目

1行目の前半

(/(?!\A)

→先頭じゃない「<」

(/(?!\A)<(?:.|\n)*?>/n

→先頭じゃない「<文字列or改行>」の塊

.gsub(/(?!\A)<(?:.|\n)*?>/n, "\n\\0")

→先頭じゃない「<文字列or改行>」の塊の前に改行を挿入

>> html="<html><head><title>test_title</title></head><body>body_sample</body></html>"
>> html.gsub(/(?!\A)<(?:.|\n)*?>/n, "\n\\0")
=> "<html>\n<head>\n<title>test_title\n</title>\n</head>\n<body>body_sample\n</body>\n</html>"


1行目の後半

.gsub(/<(?:.|\n)*?>(?!\n)/n, "\\0\n")

→「<文字列or改行>」の塊の後ろに改行がない場合、改行(\n)を挿入

なので

1行目は開始・終了タグの前後に改行を挿入していると。
>> html.gsub(/(?!\A)<(?:.|\n)*?>/n, "\n\\0").gsub(/<(?:.|\n)*?>(?!\n)/n, "\\0\n")
=> "\n\n\ntest_title\n\n\n\nbody_sample\n\n\n"


なるほど、ここまで20分。

ループの中

while end_pos = lines.index(/^<\/(\w+)/n, end_pos)

閉じタグの位置を検索

start_pos = lines.rindex(/^\s*<#{element}/ni, end_pos)

閉じタグの位置から遡って開始タグの位置を検索

lines[start_pos ... end_pos] = "__" + lines[start_pos ... end_pos].gsub(/\n(?!\z)/n, "\n" + shift) + "__"

タグの塊の中で改行を改行 + shift(第2引数:defaultスペース2個)に置換して、前後に目印「__」をセット

#irb
=> "__<html>\n  __<head>\n    __<title>\n      test_title\n    __</title>\n  __</head>\n  __<body>\n    body_sample\n  __</body>\n__</html>\n"
lines.gsub(/^((?:#{Regexp::quote(shift)})*)__(?=<\/?\w)/n, '\1')

開き(閉じ)タグの前にある目印「__」を削除

#irb
=> "<html>\n  <head>\n    <title>\n      test_title\n    </title>\n  </head>\n  <body>\n    body_sample\n  </body>\n</html>\n"

勉強になります

いまいち目印「__」をセットする意味がよくわかってなかったりしますが、
何気なく使ってるソースを見るといろんな発見があります。
(↓これとか)

  TABLE_FOR_ESCAPE_HTML__ = {
    '&' => '&amp;',
    '"' => '&quot;',
    '<' => '&lt;',
    '>' => '&gt;',
  }

  # Escape special characters in HTML, namely &\"<>
  #   CGI::escapeHTML('Usage: foo "bar" <baz>')
  #      # => "Usage: foo &quot;bar&quot; &lt;baz&gt;"
  def CGI::escapeHTML(string)
    string.gsub(/[&\"<>]/, TABLE_FOR_ESCAPE_HTML__)
  end