Wednesday, November 09, 2011

Tilt Template for ClojureScript

As a JVM language lover, I've written code mixed with more than one language. Ruby gems from Clojure, Clojure from Ruby, or other combinations. This blog is about using ClojureScript from JRuby. ClojureScript (https://github.com/clojure/clojurescript/) is "a new compiler for Clojure that targets JavaScript." ClojureScript uses Google Closure ( don't be confused! ) for optimization. So, instead of JavaScript, we can use Clojure's succinct syntax to write a JavaScript part.


As you know, Clojure is a JVM language. Clojure from JRuby is not so complicated. Many people know what's like that. My attempt here is using ClojureScript from Tilt (https://github.com/rtomayko/tilt). Tilt is a well known generic wrapper for Ruby template engines. It covers more than 20 template engines such as ERB, Haml, or CoffeeScript. Not only those, Tilt allows us to write other template wrappers. Thus, Tilt template for ClojureScript is possible.


Here's what I did. Whole code is on github, https://github.com/yokolet/clementine.

Firstly, I copied jars and Clojure code for ClojureScript under vendor/assets directory. The directory can be lib or other. An important point is to set Java's classpath to those jars and directories. See, clementine.rb below:

require 'rubygems'
require "clementine/version"
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
require "java"

CLOJURESCRIPT_HOME = File.dirname(__FILE__) + "/../vendor/assets"
$: << CLOJURESCRIPT_HOME + "/lib"
require 'clojure'

%w{compiler.jar goog.jar js.jar}.each {|name| $CLASSPATH << CLOJURESCRIPT_HOME + "/lib/" + name}
%w{clj cljs}.each {|path| $CLASSPATH << CLOJURESCRIPT_HOME + "/src/" + path}

require "clementine/clojurescript_engine"
require "clementine/clojurescript_template"
end

Line 6 sets CLOJURESCRIPT_HOME directory relative to clementine.rb. CLOJURESCRIPT_HOME + "/lib" directory has jars, so I set this directory to $LOAD_PATH so that JRuby can load clojure.jar. Line 8, "require 'clojure'" loads clojure.jar. You can also write "require 'clojure.jar'" instead, but extension doesn't matter for JRuby. Then, I added paths to $CLASSPATH. We can use $CLASSPATH global variable after "require 'java'". $CLASSPATH is Ruby Array and equivalent to Java's classpath. Line 10 and 11 set all paths to run ClojureScript.


The code below is a ClojureScriptEngine class, clojurescript_engine.rb:

require 'java'
%w{RT Keyword PersistentHashMap}.each do |name|
java_import "clojure.lang.#{name}"
end

module Clementine
class ClojureScriptEngine
def initialize(file, options)
@file = file
@options = options
end

def compile
cl_opts = PersistentHashMap.create(convert_options())
RT.loadResourceScript("cljs/closure.clj")
builder = RT.var("cljs.closure", "build")
builder.invoke(@file, cl_opts)
end

def convert_options()
opts = {}
@options.each do |k, v|
cl_key = Keyword.intern(k.to_s)
case
when (v.kind_of? Symbol)
cl_value = Keyword.intern(v.to_s)
else
cl_value = v
end
opts[cl_key] = cl_value
end
opts
end
end
end

This class basically performs ClojureScript REPL, (cljsc/build "hello.cljs" {:optimizations :advanced :output-to "hello.js"}). The function, cljsc/build, is defined in cljs/closure.clj file, so the code loads cljs/closure.clj by clojure.lang.RT.oadResourceScript("cljs/closure.clj"). Line 16 gets a reference to the method, then, line 17 invokes the method with arguments.
This gist https://gist.github.com/1350398 might be more understandable since it is straightforward.


Lastly, Tilt templete code, clojurescript_template.rb, became as in below:

require 'tilt/template'

module Clementine
class ClojureScriptTemplate < Tilt::Template
self.default_mime_type = 'application/javascript'

def self.engine_initialized?
true
end

def initialize_engine; end

def prepare
@engine = ClojureScriptEngine.new(@file, options)
end

def evaluate(scope, locals, &block)
@output ||= @engine.compile
end
end
end

Probably, you'd better to go to Tilt sites rather than to read my how-to about this code. So, I won't comment anything about this code.


Let's try ClojureScript Tilt template.

$ cd clementine
$ jruby -Ilib -S irb
jruby-1.6.5 :001 > require 'clementine'
=> true
jruby-1.6.5 :002 > require 'tilt'
=> true
jruby-1.6.5 :003 > Tilt.register(Clementine::ClojureScriptTemplate, 'cljs', 'clj')
=> ["cljs", "clj"]
jruby-1.6.5 :004 > template = Tilt.new('/Users/yoko/Works/tmp/clojurescript/hello.clj', 1, {:optimizations => :advanced})
=> #<Clementine::ClojureScriptTemplate:0x5513dd59 @compiled_method={}, @reader=#<Proc:0x517667bd@/usr/local/rvm/gems/jruby-1.6.5@jror/gems/tilt-1.3.3/lib/tilt/template.rb:67>, @engine=#<Clementine::ClojureScriptEngine:0x3494d313 @file="/Users/yoko/Works/tmp/clojurescript/hello.clj", @options={:optimizations=>:advanced}>, @options={:optimizations=>:advanced}, @line=1, @file="/Users/yoko/Works/tmp/clojurescript/hello.clj", @default_encoding=nil, @data="(ns hello)\n(defn ^:export greet []\n (str \"Hello \" n))">
jruby-1.6.5 :005 > template.render
=> "function b(a){throw a;}var f=true,h=null,j=false;function aa(){return function(a){return a}}function k(a){return function(){return this[a]}}function l(a){return function(){return
(snip)



It worked!


This is a preliminary implementation and far from a release at this moment. But, I could confirm ClojureScript is available from Ruby. ClojureScript will be added to Ruby's template engines. My next goal will be ClojureScript from Ruby web application frameworks. It'll be more practical.

No comments: