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>相当のものは入れられないのかな。強調したい箇所が強調できないから、すごく不満なんだけど。