Wednesday, January 23, 2013

compiling and running coffeescript from within python

Running javascript from within python is already very easy using pyv8, the python wrapper for google's V8 javascript engine, like e.g. used within the Chrome browser.

On the other hand Coffeescript is a cool little language that compiles into javascript. I like it a lot more then javascript :)

I want to work with coffeescript from within python, without calling external tools.

The complication is that pyv8 provides a strict javascript environment. On the other hand coffeescript uses node.js and assumes all of it's libraries are available. (e.g. file I/O)

As the file I/O is not needed for my purpose, I just commented out the parts from the coffeescript code that use it.

The main missing part is the 'require' directive coffeescript uses from node.js, but which is not available when using plain V8 javascript. This can quite trivially be implemented in python.

However, this is still not enough, because of the way the functional wrapping is implemented in the javascript coffeescript generated for itself (coffeescript is written in coffeescript!) I had to change that generated code to explicitly return the "exports" object towards the caller of "require" because it would just return null by default. (+ a couple of other minor hacks here and there)

Here is all the python code:

 1 #!/usr/bin/env python
 2 #
 3 # (c) 2012 Joost Yervante Damad <joost@damad.be>
 4 #
 5 # License: CC0 http://creativecommons.org/publicdomain/zero/1.0/
 6 
 7 import PyV8
 8 import os.path
 9 
10 class Global(PyV8.JSClass):
11 
12     def __init__(self):
13       self.path = ""
14 
15     def require(self, arg):
16       content = ""
17       with open(self.path+arg+".js") as file:
18         file_content = file.read()
19       result = None
20       try:
21         store_path = self.path
22         self.path = self.path + os.path.dirname(arg) + "/"
23         result = PyV8.JSContext.current.eval(file_content)
24       finally:
25         self.path = store_path
26       return result
27 
28 def make_js(coffee_script_code):
29   with PyV8.JSContext(Global()) as ctxt:
30     js_make_js_from_coffee = ctxt.eval("""
31 (function (coffee_code) {
32   CoffeeScript = require('coffee-script/coffee-script');
33   js_code = CoffeeScript.compile(coffee_code, null);
34   return js_code;
35 })
36 """)
37     return js_make_js_from_coffee(coffee_script_code)
38 
39 coffee_script_code = """
40 yearsOld = max: 10, ida: 9, tim: 11
41 
42 ages = for child, age of yearsOld
43   "#{child} is #{age}"
44 return ages
45 """
46 
47 js_code = make_js(coffee_script_code)
48 
49 print js_code
50 
51 with PyV8.JSContext() as ctxt:
52   print PyV8.convert(ctxt.eval(js_code))


This is the output javascript from running the script:



 1 (function() {
 2   var age, ages, child, yearsOld;
 3 
 4   yearsOld = {
 5     max: 10,
 6     ida: 9,
 7     tim: 11
 8   };
 9 
10   ages = (function() {
11     var _results;
12     _results = [];
13     for (child in yearsOld) {
14       age = yearsOld[child];
15       _results.push("" + child + " is " + age);
16     }
17     return _results;
18   })();
19 
20   return ages;
21 
22 }).call(this);
23 

And this is the result of the execution of the coffeescript:
['max is 10', 'ida is 9', 'tim is 11']

fun!