Ruby to Javascript in 100 LOC, is it possible ?

The goal of this tutorial is to show you how, with Ruby, we can turn this:

if ($('a').value == 'a') {
  doThis(true, 'maybe');
  andThis($('a').innerHTML);
}
var MyClass = Class.create();
MyClass.prototype = {
  myMethod: function(arg1, arg2) {
    cool();
  },
  anotherOne: function() {
    say('hi');
  },
};

Into Ruby code, with a 100 lines Ruby script.

Go grab a beer / coffee / Guru / water / Asian pear / Pad Thai and get ready !

# First we build a base Page class that will handle the common statement and hold the output.
# This is pretty simple, as you can see, we only output text.
class Page
  def initialize
    @output = ''
  end
  
  def if(condition)
    self << "if (#{condition}) {"
    yield self
    self << "}"
  end
  
  def def(method, *args)
    self << "#{method}: function(#{args * ', '}) {"
    yield self
    self << "},"
  end
  
  def class(name, &block)
    self << "var #{name} = Class.create();"
    self << "#{name}.prototype = {"
    yield self
    self << "};"
  end
  
  def <<(output)
    @output << output << "\n"
  end
  
  def to_s
    @output
  end
end

page = Page.new

# Using those methods looks pretty cool
# but we still need to write some js code.
page.if "$('a').value == 'a'" do
  page << "doThis(true, 'maybe');"
  page << "andThis($('a').innerHTML);"
end
page.class :MyClass do
  page.def 'myMethod', :arg1, :arg2 do
    page << "cool();"
  end
  page.def 'anotherOne' do
    page << "say('hi');"
  end
end

puts page

# Outputs
if ($('a').value == 'a') {
doThis(true, 'maybe');
andThis($('a').innerHTML);
}
var MyClass = Class.create();
MyClass.prototype = {
myMethod: function(arg1, arg2) {
cool();
},
anotherOne: function() {
say('hi');
},
};

# Iteration 2 - Make the outputed code beautiful

# Lets add some helper methods to all the objects
# so we can make beautiful Ruby code output as
# beautiful Javascript.
class Object
  # This method will allow to use Ruby naming convention
  # to declare Javascript methods.
  # Converting my_method to myMethod
  def to_js_method
    to_s.gsub(/_(\w)/) { $1.upcase }
  end
  
  # This one will convert an object to it's Javascript
  # equivalent. This is pretty basic here. Wrap string in '
  # or outputing if any other type.
  def to_js_arg
    self.is_a?(String) ? "'#{self}'" : self
  end
end

class Page
  def initialize
    @output = ''
    @indent = 0
  end
  
  # We make the outputed code a little more pretty
  # by indenting it! See the indented method bellow.
  def if(condition)
    self << "if (#{condition}) {"
    indented { yield self }
    self << "}"
  end
  
  ...
  
  # If an unexisting method is called, output it
  # as a Javascript method call.
  def method_missing(method, *args)
    self << "#{method.to_js_method}(#{args.collect { |arg| arg.to_js_arg } * ', '});"
  end
  
  def <<(output)
    @output << '  ' * @indent << output << "\n"
  end
  
  private
    def indented
      @indent += 1
      yield
      @indent -= 1
    end
end

page = Page.new

page.if "$('a').value == 'a'" do
  # As you can see, we use the method_missing magic
  # to create method calls
  page.do_this true, 'maybe'
  page.and_this "$('a').innerHTML"
end
page.class :MyClass do
  page.def :my_method, :arg1, :arg2 do
    page.cool
  end
  page.def :another_one do
    page.say 'hi'
  end
end

puts page

# Nice camelized method names, clearly indented = sweet!
if ($('a').value == 'a') {
  doThis(true, 'maybe');
  andThis('$('a').innerHTML');
}
var MyClass = Class.create();
MyClass.prototype = {
  myMethod: function(arg1, arg2) {
    cool();
  },
  anotherOne: function() {
    say('hi');
  },
};

# Iteration 3 - converting Ruby expressions to Javascript

# Now lets take care of that if statement.
# In Ruby, even operators are methods. We can take advantage of this
# and override those and output what we want!

# This module will override all comparison operators
# and output a string representation of the expression.
# So calling a == 'a' will return the string "a == 'a'"
module JSComparable
  def self.included(base)
    base.class_eval do
      %w(== < > <= >=).each do |operator|
        define_method operator do |other|
          [self.to_js_arg, operator, other.to_js_arg] * ' '
        end
      end
    end
  end
end

# We need to defined a simple class in which we will
# override those operators.
class Element
  attr_reader :id
  include JSComparable
  
  def initialize(id)
    @id = id
  end
  
  def to_s
    "$('#{id}')"
  end
end

# Since we're lazy, we'd like to type E[:a] ratter then Element.new(:a)
module E
  def self.[](id)
    Element.new id
  end
end

# All this allows us to rewrite this:
page.if "$('a') == 'a'" do
# into this:
page.if E['a'] == 'a' do
# Pure Ruby, how cool ?

###############
# Iteration 4 #
###############
# Now one line is still pretty annoying:
#   page.and_this "$('a').innerHTML"
# To turn this into Ruby code we can use a little
# method_missing magic again.

# We'll create a small class that will handle
# the calls to methods on element.
# When rendered, parent.method is outputed.
class MethodProxy
  include JSComparable
  
  def initialize(parent, name)
    @parent = parent
    @name = name
  end
  
  def to_s
    [@parent, @name.to_js_method] * '.'
  end
end

# Add a simple hook into Element
# so any unknow method calls are rendered as
# Javascript method calls.
class Element
  ...
  
  def method_missing(method, *args)
    MethodProxy.new(self, method)
  end
end

# All this allows us to come to the almost perfect:
page.if E['a'].value == 'a' do
  page.do_this true, 'maybe'
  page.and_this E[:a].innerHTML
end
page.class :MyClass do
  page.def :my_method, :arg1, :arg2 do
    page.cool
  end
  page.def :another_one do
    page.say 'hi'
  end
end

# Iteration 5 - Final touch, making this a real language !

# The 'page.' thing is pretty annoying I know!
# What's the simpless thing we could do to remove this ?

page = Page.new

code = File.read(ARGV[0])
code.gsub! /(if|class|def) (.*)$/, 'page.\1 \2 do'
eval code

puts page

# Now place this in a seperate file:
if E[:a].value == 'a'
  page.do_this(true, 'maybe')
  page.and_this(E[:a].innerHTML)
end

class :MyClass
  def :my_method, :arg1, :arg2
    page.cool
  end
  def :another_one
    page.say('hi')
  end
end

# And run with: ruby js.rb javascript.rjs
# and you'll get:

if ($('a').value == 'a') {
  doThis(true, 'maybe');
  andThis($('a').innerHTML);
}
var MyClass = Class.create();
MyClass.prototype = {
  myMethod: function(arg1, arg2) {
    cool();
  },
  anotherOne: function() {
    say('hi');
  },
};

If you’re serious about this, I’d suggest you take a look at ParseTree which allows you to explore the parse tree of some Ruby code.

I hope this helps anyone understand better some of Ruby meta-programming capability, beauty and power!

Ruby, I love you!

Amen

Get the full script here (rename it to js.rb)

11 Comments

Filed under js, ruby, tutorial

11 responses to “Ruby to Javascript in 100 LOC, is it possible ?

  1. This is converting Ruby code to Javascript code using a Ruby program right?

    So can you convert Ruby on Rails into Rhino on Rails?

    Another thourght – can the program convert itself, run the result on Rhino and still work?

    A sort of “eat your own dog food”.

  2. That’s right Treblid, it converts Ruby code to JS using a Ruby script

    But it would require a lot more work to convert a RoR app. I haven’t implemented the whole js language here and I beleive Rails and Rhino are not mapped one to one.

    What do you mean by “convert itself”? That sounds like black magic, end of the world kind of stuff! I’m scared!

  3. I think the answer is no as you “haven’t implemented the whole js language” (should that be Ruby language?). Now that would be REALLY impressive…

    I’m not that familiar with Ruby but say you run it as follows,
    foo bar.rb
    and this produces bar.js, then try
    foo foo.rb
    to get foo.js
    Then in theory you would have a javascript program that converts Ruby to javascript. To run the javascript program you would need Rhino (from Mozilla it runs JS on the java Virtual Machine).

    If it was this capable, as Rails is written in Ruby, you could in theory create a JS Framework by converting Rails’s Ruby programs.

  4. With JRuby, Ruby runs on the Java VM.

    I don’t know if it supports Rails already but it will in a near future I’m sure.

  5. With prototype trying so hard to bring rubyishness to javascript, I almost think ruby-to-js conversion would be possible.

    Before rails, I was working with Google Web Toolkit quite a bit. With a code converter, a library like that might be possible on top of rails.

    Anybody want to give it a try with me?

  6. I think the problem with code converter is that you always end up needing to fix that tiny little details that requires you to fall down to the actual language. And I don’t find Javascript that bad, I fact, I like javascript! Specially with prototype!

    But what would be cool is adding if statement to Rails RJS template, now that would be awesome!

    page.if page[:block].visible do
    page[:block].update ‘This is visible’
    end

    Thx for the comment James!

  7. Greetings! Quick question that’s entirely off topic. Do you know how to make your site mobile friendly? My blog looks weird when browsing from my iphone 4. I’m trying to find a theme
    or plugin that might be able to resolve this issue. If you
    have any recommendations, please share. Appreciate it!

  8. I know this if off topic but I’m looking into starting my own weblog and was wondering what all is required to get set up? I’m assuming having a blog like yours
    would cost a pretty penny? I’m not very web savvy so I’m
    not 100% certain. Any recommendations or advice would be greatly appreciated. Kudos

  9. Today, I went to the beach front with my kids.
    I found a sea shell and gave it to my 4 year old
    daughter and said “You can hear the ocean if you put this to your ear.” She put the shell to her ear and screamed.
    There was a hermit crab inside and it pinched
    her ear. She never wants to go back! LoL I know this
    is entirely off topic but I had to tell someone!

  10. wonderful publish, very informative. I ponder why the opposite experts of this sector do not
    realize this. You should proceed your writing. I am confident,
    you have a great readers’ base already!

  11. Thank you for the good writeup. It in fact was a
    amusement account it. Look advanced to far added agreeable from you!
    By the way, how could we communicate?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s