MozReplをRubyからシームレスに使う

MozReplでいろいろ遊んでいます。なかなか難しいですが、だんだん面白くなっています。MozReplを経由してちょうどWin32OLEみたいにRubyのオブジェクトとして、Javascriptのオブジェクトを扱えるようなクラスJSobjectを作ってみました。

こんな感じでまるでRubyを使っているかのようにJavascriptのオブジェクトが扱えます。

# サンプル

# Helloのダイアログを出す
$window.alert("Hello")

# DOMを操作してみる
puts $document.childNodes.item(0)

# 自分の見ているURLの表示(高速インタフェース研究会より)
puts $content.location.href

ソースコードを下に晒しますが、とても汚いつくりです。特に、Rubyで参照したオブジェクトはGCされずにどんどんJavascriptの配列に溜まっていきます。なんとか、GCしたいのですが、結構面倒な話です。ちょうど分散GCと同じ問題が発生します。
他にも、色々問題があると思います。でも、RubyMozillaをいじって*遊ぶには*には便利じゃないかなと思います。

変更履歴
  2008/1/20   いくつかのバグの修正(FireFoxに渡すJavascriptの括弧が閉じていないとか、セミコロンがないとか)
              inspectメソッドを追加、これでpが使えるようになった
require 'net/telnet'

# Javascriptの表現形式への変換
class Object
  def to_js
    self
  end
end

class String
  def to_js
    '"' + self + '"'
  end
end

# Mozillaとの通信等の縁の下
module JSCommon
  PROMPT = /(^repl> )|(^\.\.\.\.> )/
#  @@prompt = /^repl>/
  @@telnet =  Net::Telnet.new({ "Host" => "localhost", 
                                "Port" => 4242,
                                "Prompt" => PROMPT,
                                "Telnetmode" => false})
  @@telnet.waitfor(PROMPT)

  private
  def js_exec(com)
    res = nil
    @@telnet.cmd(com) {|c|
      res = c.gsub(PROMPT, "")
      if $DEBUG
        p com
        p res
      end
      if /^\s*[0-9]+\s*$/ =~ res then
        res = res.to_i
      end
    }

    res
  end

  def make_js_args(args)
    args.inject([]) {|res, arg|
      res.push arg.to_js
    }.join(',')
  end
end

# Javascriptのオブジェクトのwapper。実体は、__objtという配列のインデックス
class JSObject
  include JSCommon
  
  # この部分はdelgate.rbから引用しています。
  preserved = [:__id__, :object_id, :__send__, :invoke_method, :respond_to?, :send]
  instance_methods.each do |m|
    next if preserved.include?(m)
    undef_method m
  end

  def initialize(id)
    @id = id
  end

  def method_missing(name, *args)
    if /([^=]*)=/ =~ name.to_s then
      js_exec("__objt[#{@id}].#{$1} = #{args[0].to_js};")
      args[0]
    else
      rs = js_exec("__objt.push(__objt[#{@id}].#{name}(#{make_js_args(args)}));")
      if !rs.is_a?(Integer) then
        rs = js_exec("__objt.push(__objt[#{@id}].#{name});")
      end
        
      if rs.is_a?(Integer) then
        JSObject.new(rs - 1)
      else
        rs
      end
    end
  end

  def to_s
    js_exec("__objt[#{@id}]")
  end

  def inspect
    js_exec("repl.inspect(__objt[#{@id}]);")
  end
  
  def to_js
    "__objt[#{@id}]"
  end
end

# 特に定義していなくても使えるJavascriptの変数等の宣言
class JSTopLevel
  extend JSCommon
  @@jstoplevel = nil
  def self.refvar(name)
    if @@jstoplevel == nil then
      js_exec("var __objt = new Array();")
      js_exec("__objt.push(window);")
      js_exec("__objt.push(document);")
      js_exec("__objt.push(content);")
      js_exec("__objt.push(repl);")
      @@jstoplevel = Hash.new(nil)
      @@jstoplevel['window'] = JSObject.new(0)
      @@jstoplevel['document'] = JSObject.new(1)
      @@jstoplevel['content'] = JSObject.new(2)
      repl = JSObject.new(3)
      @@jstoplevel['repl'] = repl
    end
    @@jstoplevel[name]
  end
end

# お好みに応じて定義してください。
$window =  JSTopLevel.refvar('window')
$document =  JSTopLevel.refvar('document')
$content =  JSTopLevel.refvar('content')
$repl =  JSTopLevel.refvar('repl')

if __FILE__ == $0 then
  # サンプル
  
  # Helloのダイアログを出す
  #$window.alert("Hello")
  
  # DOMを操作してみる
  #puts $document.childNodes.item(0)
  
  # 自分の見ているURLの表示(高速インタフェース研究会より)
  #puts $content.location.href
  
  #p $content.document
  
end