ripper 週末クッキング その0
ripper 週末クッキング その1 ripperはどこに?
ripperってどこに転がってるんだっけ?いきなり見つかりません。
http://rubyforge.org/projects/ripper/
のCVSから拾ってくればいいのかな。なんか古そうだけど。最近、1.9に統合されたという話を見るのだが、1.8系に対応したripperってもう開発ストップ?
さすがに見切り発車すぎて クッキングシリーズ三段目にして途絶の危機か?
CVSからtarボールをひろってきて、ruby extconf.rb;make;するもエラー。gprefがなかったらしい。apt-get install gprefして続行。
今度は、ripper.cのコンパイルでこけた。
… なんか駄目ぽ。明日にしよう。
やりなおしたら、すっと通りました。今度は、make testがこける。test/unitがないらしい。再びapt-get install libtest-unit-ruby。おーし。テストとおった。おめでとう自分。
というわけでインストールは完了。なんのことはない、必要なものが全てそろっていたら一発でいくはずでしたね。
ripper 週末クッキング その2 ripperのお勉強
まずサンプルをたぐってみる。
count-if.rbがやることがわかりやすくてよいかも。短いし、ということで読む。
わかったこと
- Ripperを継承したクラスが基本
- Ripperには、解析する文字列を与えてnew
- メソッドを定義して、parseで解析開始。あとはメソッドまかせ?確かSAX的に使うという話だったので、解析結果の要素にしたがって順に対応するメソッドが呼ばれるのだろう。
- on__kw(word)というメソッドでキーワードが拾えるらしい。wordはキーワードの種類だろう。中身は文字列かな?キーワードが来た!というイベントに対応するメソッドですね。
- method_missingも定義されているが、こちらは何をしているかいまいちわからず。
というわけで、一番謎なmethod_missingを、引数の中身を表示させるように書きかえてみる。
実行。ぽち。
:on__spというシンボルと、空白の文字列(個数はいろいろ)がかえってきている。要は、spというイベントを発生させたいのだがそれに対応するメソッドがない、ということかな。その後のはソース中の文字列とみた。
というわけで、def on__sp(word)して、wordを表示させてみた。予想どおり空白文字がどどっと表示された。ふむ。要は空白は無視されるはずだから対応するメソッドは記述されてない、ってことかな。
てことは、Ripperが発行するイベントを全て把握すれば使えるようになるはずだな。ついでに、on__kwで補足されるwordも表示してみた。def,do,end,if,elsif等々、キーワードが文字列で拾えてますね。よし。
ripper 週末クッキング その3 ripperが吐くイベント
on__spは定義されてなかったけど、method_missingで拾えたのがon__spくらいだったので、他のメソッド(イベント)はあらかじめ定義されていると当りをつけて、lib/ripper.rbを読んでみた。予想どおり、on__xxなるメソッドが羅列されている。Parser EventsとLexer Eventに分かれているらしい。わかりやすいコメントですね。
イベントの種類としては、Parser Eventsとして定義されているやつを押さえれば十分だろう。Lexer Eventsはon__scanを除いてひとまず無視。on__scan(event,token)という中身のないメソッドがあるが、これは恐らくイベント生成時のフック用だと予想して、またもeventとtokenを表示させるものを書いてみた。結果はビンゴ。event名のシンボルとその時のtokenが羅列されました。
というわけで、「ここはどういうイベントになるんだー?」と思ったときは、上記の内容表示on__scanを書いたものでソースを解析させれば、そのトークンが何のイベントを発生させるかわかるわけですね。たぶんParser Eventsのメソッド名みたら予想つくと思うけど。よし。
ripper 週末クッキング その4 できました
まず、いろいろ勘違いしてたところを訂正。
on__scanが返すイベントは、Parser Eventsじゃなくて、Lexer Eventsのほうだった。Lexerのほうが字句解析なんだから、スキャナーのon_scanでフックできるイベントはLexerのほうだよね。
そして、ハイライトするためには構文解析なんて必要なくて、字句解析のレベルでそのトークンが何かわかればよかった。なので、Parser Eventsじゃなくて、Lexer Eventsのほうを使った。
でも、字句解析レベルだとヒアドキュメント、文字列等の「その中に違う字句を持つけど、ひとくくりにして扱いたい部分」を自分で管理する必要があった。このへん、もしかしたらParser Eventsと組みあわせてやれば楽にできたのかもしれないけど。
というわけで、on__kw等の色付けに必要な部分のLexer Eventに対応するメソッドを書いて、状態管理の必要なところはon__scanの中で管理。状態遷移の管理がはいったので、on__scanがちょっと複雑になっちゃいました。
できたもの
それぞれ以下のような色付けをします。TAB幅は8に決めうちです。色はCSSを埋めこんで制御してるので適当にいじれば適当に変わります。
今度は、Lexer EventだけじゃなくてParser Eventの処理が必要なのを作ってみないと駄目だな。
せっかくなのでソース自体をHTML化したやつをはりつけておこう。テスト用に変なのもまじってますが。
と思ったら、はてなではHTMLタグが無効化されるのかな?よくわからない。すこし格闘してみたが、最終的には
がちゃんと機能してくれないのであきらめ。
brを出力するのをやめて、pとCSSで制御するようにした。ふー。
はてな用にCSSを微調整してはりつけ。えらい苦労した。
そうか、よく考えたら一段階目の出力は抽象的な構造にしといて、はてなやらWikiやら普通のHTMLやら出力先によってシリアライザーを書きわけるべきだったか。
ripper 週末クッキング その5 シリアライザーの分離
というわけで、解析部分とシリアライズ部分を分けてみた。
いまのとこ、素のHTMLを一枚生成するのと、はてな用の文字列を生成するのと二種類。
ソースはこんなかんじ。
とおもったら、
|と< >
が干渉して途中でおかしくなりやがりますね。やれやれ。強引に置換することにする。
# highlight.rb by sshi
$global = 1
test = /regexp/
require 'ripper'
require 'cgi'
hoge = :test;:test
class HighLight < Ripper
def self.highlight(src)
obj = new(src)
obj.parse
obj.ret
end
attr_reader :ret
def initialize(src)
super src
@ret = []
end
def serialize(serializer)
serializer.new(@ret).process
end
def add(word,klass = nil)
if klass == nil
@ret << word.gsub(/\n$/,'')
else
@ret << [klass,word.gsub(/\n$/,' ')]
end
@ret << ['newline',nil] if word =~ /\n$/
end
@@event_list = %w(kw gvar int ivar cvar const op embdoc embdoc_beg embdoc_end comment)
@@sym_list = @@event_list.map do | event |
"on__#{event}".to_sym
end
@@event_list.each do | event |
self.class_eval<<EOS
def on__#{event}(word)
add(word,'#{event}')
end
EOS
end
def on__scan(event,token)
case @state
when :heredoc
if event == :on__heredoc_end
@state = :none
add(token.gsub(/\n$/,''),"heredoc")
else
add(token,"heredoc")
end
when :sym
add(token,'sym')
@state = :none
when :tstring
add(token,'tstring')
@string_stack += 1 if event == :on__tstring_beg
if event == :on__tstring_end
@string_stack -= 1
@state = :none if @string_stack == 0
end
when :regexp
add(token,'regexp')
@state = :none if event == :on__regexp_end
else
case event
when :on__heredoc_beg
add(token+"\n",'heredoc')
@state = :heredoc
when :on__tstring_beg
add(token,'tstring')
@state = :tstring
@string_stack = 1
when :on__symbeg
add(token,'sym')
@state = :sym
add(token,'regexp')
@state = :regexp
else
add(token) unless @@sym_list.include?(event)
end
end
end
def method_missing( mid, *args );end
end
require 'cgi'
class HTMLSerializer
def initialize(code_ar)
@ar = code_ar
end
def e(str)
return CGI.escapeHTML(str).gsub(/ /,' ').gsub(/\t/,' '*8)
end
def contents
@ret = ""
@ar.each do | item |
if item.class == String
@ret += e(item)
else
klass = item[0]
word = item[1]
if klass == 'newline'
@ret += " </p>\n<p class='br'>"
else
@ret +="<span class='#{klass}'>#{e(word)}</span>"
end
end
end
@ret
end
def process
return<<EOS_h
<html>
<head>
<style>
body {
font-family:monospace;
}
.heredoc {
color:#ff00ff;
}
.comment ,.embdoc,.embdoc_beg,.embdoc_end{
color:gray;
}
.kw ,.op{
color:red;
}
.const {
color:maroon;
}
.tstring ,.sym,.regexp {
color:blue;
}
.gvar , .cvar , .ivar {
color:green;
}
.int {
color:blue;
}
p.br {
margin:0pt;
padding:0pt;
}
</style></head>
<body>
<p class='br'>
#{contents}
</p>
</body>
</html>
EOS_h
end
end
class HATENA < HTMLSerializer
def process
return<<EOS_ha
><div class="highlight" style="overflow:scroll;">
<p class='br'>
#{contents.gsub(/\ | /,' | ')}
</p>
</div><
EOS_ha
end
end
if __FILE__ == $0
p = HighLight.new(ARGF)
p.parse
#puts p.serialize(HTMLSerializer)
puts p.serialize(HATENA)
end
はてな用の文字列を生成して、はてなダイアリーに貼り付ける時には、あらかじめCSSを追加しておかないといけない。例えばこんなの。
.heredoc { color:#ff00ff; } .comment ,.embdoc,.embdoc_beg,.embdoc_end{ color:gray; } .kw ,.op{ color:red; } .const { color:maroon; } .tstring ,.sym,.regexp { color:blue; } .gvar , .cvar , .ivar { color:green; } .int { color:blue; } div.highlight { font-family:monospace; background:white; padding:0pt; margin:0pt; overflow:scroll; font-size: x-small; } div.highlight p.br { margin:0; padding:0; text-indent: 0em; line-height:1; }
HTML用に生成した時は、これらのスタイルは自動的に埋めこまれる。