eRubyを35行で実装してみる
以前のエントリで「eRubyは50行もあれば実装可能」と書いたけど、eRuby の実装は正規表現を使えば極めて簡単。Erubisには約50行で実装されたtiny.rbが含まれているけど、これをさらに小さくしてみたら、35行で実装できた。code golfみたいなことは一切せず、空行を省いたりもしていない。
class TinyEruby def initialize(input=nil) @src = convert(input) if input end attr_reader :src def result(_binding=TOPLEVEL_BINDING) eval @src, _binding end def convert(input) src = "_buf = '';" # preamble input.scan(/(.*?)<%(=)?(.*?)%>/m) do |text, ch, code| src << " _buf << '#{escape_text(text)}';" unless text.empty? if ch == '=' # expression src << " _buf << (#{code}).to_s;" else # statement src << code << ';' end end rest = $' || input src << " _buf << '#{escape_text(rest)}';" unless rest.empty? src << "\n_buf.to_s\n" # postamble return src end private def escape_text(text) return text.gsub!(/['\\]/, '\\\\\&') || text end end
expressionとstatementの部分をまとめて
src << (ch == '=' ? " _buf << (#{code}).to_s" : code ) << ";"
とかにすれば30行を切ることもできるだろうけど、そこまではしない。
ただ、これだと正規表現でのグルーピングのせいで極めて遅くなる場合があるので、ここでやっているように正規表現から遅いグルーピングを取り除く。これで40行。
class TinyEruby def initialize(input=nil) @src = convert(input) if input end attr_reader :src def result(_binding=TOPLEVEL_BINDING) eval @src, _binding end def convert(input) src = "_buf = '';" # preamble pos = 0 input.scan(/<%(=)?(.*?)%>/m) do |ch, code| match = Regexp.last_match len = match.begin(0) - pos text = input[pos, len] pos = match.end(0) src << " _buf << '#{escape_text(text)}';" unless text.empty? if ch == '=' # expression src << " _buf << (#{code}).to_s;" else # statement src << code << ';' end end rest = $' || input src << " _buf << '#{escape_text(rest)}';" unless rest.empty? src << "\n_buf.to_s\n" # postamble return src end private def escape_text(text) return text.gsub!(/['\\]/, '\\\\\&') || text end end
またコメント (<%# %>) にも対応してみる。ついでにSmart Trim対応、つまり文の場合は前後の空白を取り除くようにしてみる (式の場合は何もしない)。こうすると、出力に余計な空行が含まれないようになる。これで52行。
class TinyEruby def initialize(input=nil) @src = convert(input) if input end attr_reader :src def result(_binding=TOPLEVEL_BINDING) eval @src, _binding end def convert(input) src = "_buf = '';" # preamble pos = 0 input.scan(/(^[ \t]*)?<%([=\#])?(.*?)%>([ \t]*\r?\n)?/m) do |lspace, ch, code, rspace| match = Regexp.last_match len = match.begin(0) - pos text = input[pos, len] pos = match.end(0) src << build_text(text) if ch == '=' # expression src << t(lspace) << " _buf << (#{code}).to_s;" << t(rspace) elsif ch == '#' # comment src << t(lspace) << ("\n" * code.count("\n")) << t(rspace) else # statement if lspace && rspace src << "#{lspace}#{code}#{rspace};" else src << t(lspace) << code << ';' << t(rspace) end end end rest = $' || input src << build_text(rest) src << "\n_buf.to_s\n" # postamble return src end private def build_text(text) return text && !text.empty? ? " _buf << '#{escape_text(text)}';" : '' end alias t build_text def escape_text(text) return text.gsub!(/['\\]/, '\\\\\&') || text end end
このくらい簡単だと、他の言語で実装するのも簡単 (Pythonは除く)。興味のある人は移植してみるべし。
しかし、はてなのpre記法は中に<strong>相当のものは入れられないのかな。強調したい箇所が強調できないから、すごく不満なんだけど。