Minimal yet extensible Ruby to JavaScript conversion.
The base package maps Ruby syntax to JavaScript semantics. For example:
- a Ruby Hash literal becomes a JavaScript Object literal
- Ruby symbols become JavaScript strings.
- Ruby method calls become JavaScript function calls IF there are either one or more arguments passed OR parenthesis are used
- otherwise Ruby method calls become JavaScript property accesses.
- by default, methods and procs return
undefined - splats mapped to spread syntax when ES2015 or later is selected, and
to equivalents using
apply,concat,slice, andargumentsotherwise. - ruby string interpolation is expanded into string + operations
andandorbecome&&and||a ** bbecomesMath.pow(a,b)<< abecomes.push(a)unlessbecomesif !untilbecomeswhile !caseandwhenbecomesswitchandcase- ruby for loops become js for loops
(1...4).step(2){becomesfor (var i = 1; i < 4; i += 2) {x.forEach { next }becomesx.forEach(function() {return})lambda {}andproc {}becomesfunction() {}class Person; endbecomesfunction Person() {}- instance methods become prototype methods
- instance variables become underscored,
@namebecomesthis._name - self is assigned to this is if used
- Any block becomes and explicit argument
new Promise do; y(); endbecomesnew Promise(function() {y()}) - regular expressions are mapped to js
raisebecomesthrow- expressions enclosed in backtick operators (``) and
%x{}literals are evaluated in the context of the caller and the results are inserted into the generated JavaScript.
Ruby attribute accessors, methods defined with no parameters and no parenthesis, as well as setter method definitions, are mapped to Object.defineProperty, so avoid these if you wish to target users running IE8 or lower.
While both Ruby and JavaScript have open classes, Ruby unifies the syntax for
defining and extending an existing class, whereas JavaScript does not. This
means that Ruby2JS needs to be told when a class is being extended, which is
done by prepending the class keyword with two plus signs, thus:
++class C; ...; end.
Filters may be provided to add Ruby-specific or framework specific behavior. Filters are essentially macro facilities that operate on an AST representation of the code.
See notimplemented_spec for a list of Ruby features known to be not implemented.
Basic:
require 'ruby2js'
puts Ruby2JS.convert("a={age:3}\na.age+=1")With filter:
require 'ruby2js/filter/functions'
puts Ruby2JS.convert('"2A".to_i(16)')Enable ES2015 support:
puts Ruby2JS.convert('"#{a}"', eslevel: 2015)Enable strict support:
puts Ruby2JS.convert('a=1', strict: true)With ExecJS:
require 'ruby2js/execjs'
require 'date'
context = Ruby2JS.compile(Date.today.strftime('d = new Date(%Y, %-m-1, %-d)'))
puts context.eval('d.getYear()')+1900Conversions can be explored interactively using the demo provided.
JavaScript is a language where 0 is considered false, strings are
immutable, and the behaviors for operators like == are, at best,
convoluted.
Any attempt to bridge the semantics of Ruby and JavaScript will involve trade-offs. Consider the following expression:
a[-1]Programmers who are familiar with Ruby will recognize that this returns the
last element (or character) of an array (or string). However, the meaning is
quite different if a is a Hash.
One way to resolve this is to change the way indexing operators are evaluated, and to provide a runtime library that adds properties to global JavaScript objects to handle this. This is the approach that Opal takes. It is a fine approach, with a number of benefits. It also has some notable drawbacks. For example, readability and compatibility with other frameworks.
Another approach is to simply accept JavaScript semantics for what they are.
This would mean that negative indexes would return undefined for arrays
and strings. This is the base approach provided by ruby2js.
A third approach would be to do static transformations on the source in order
to address common usage patterns or idioms. These transformations can even be
occasionally unsafe, as long as the transformations themselves are opt-in.
ruby2js provides a number of such filters, including one that handles negative
indexes when passed as a literal. As indicated above, this is unsafe in that
it will do the wrong thing when it encounters a hash index which is expressed
as a literal constant negative one. My experience is that such is rare enough
to be safely ignored, but YMMV. More troublesome, this also won’t work when
the index is not a literal (e.g., a[n]) and the index happens to be
negative at runtime.
This quickly gets into gray areas. each in Ruby is a common method that
facilitates iteration over arrays. forEach is the JavaScript equivalent.
Mapping this is fine until you start using a framework like jQuery which
provides a function named each.
Fortunately, Ruby provides ? and ! as legal suffixes for method names,
Ruby2js filters do an exact match, so if you select a filter that maps each
to forEach, each! will pass through the filter. The final code that emits
JavaScript function calls and parameter accesses will strip off these
suffixes.
This approach works well if it is an occasional change, but if the usage is
pervasive, most filters support options to exclude a list of mappings,
for example:
puts Ruby2JS.convert('jQuery("li").each {|index| ...}', exclude: :each)Alternatively, you can change the default:
Ruby2JS::Filter.exclude :eachStatic transformations and runtime libraries aren't aren’t mutually exclusive.
With enough of each, one could reproduce any functionality desired. Just be
forewarned, that implementing a function like method_missing would require a
lot of work.
While this is a low level library suitable for DIY integration, one of the obvious uses of a tool that produces JavaScript is by web servers. Ruby2JS includes three such integrations:
As you might expect, CGI is a bit sluggish. By contrast, Sinatra and Rails are quite speedy as the bulk of the time is spent on the initial load of the required libraries.
In general, making use of a filter is as simple as requiring it. If multiple filters are selected, they will all be applied in parallel in one pass through the script.
-
return adds
returnto the last expression in functions. -
require supports
requireandrequire_relativestatements. Contents of files that are required are converted to JavaScript and expanded inline.requirefunction calls in expressions are left alone. -
camelCase converts
underscore_casetocamelCase. See camelCase_spec for examples. -
.all?becomes.every.any?becomes.some.chrbecomesfromCharCode.clearbecomes.length = 0.deletebecomesdelete target[arg].downcasebecomes.toLowerCase.eachbecomesfor (i in ...) {}.each_keybecomesObject.keys().forEach.each_valuebecomesfor (i in ...) {}.each_with_indexbecomes.forEach.end_with?becomes.slice(-arg.length) == arg.empty?becomes.length == 0.find_indexbecomesfindIndex.firstbecomes[0].first(n)becomes.slice(0, n).gsubbecomesreplace //g.include?becomes.indexOf() != -1.inspectbecomesJSON.stringify().keysbecomesObject.keys().lastbecomes[*.length-1].last(n)becomes.slice(*.length-1, *.length).maxbecomesMath.max.apply(Math).merge!becomesObject.assign().minbecomesMath.min.apply(Math).nil?becomes== null.ordbecomescharCodeAt(0)putsbecomesconsole.log.replacebecomes.length = 0; ...push.apply(*).respond_to?becomesright in left.start_with?becomes.substring(0, arg.length) == arg.upto(lim)becomesfor (var i=num; i<=lim; i+=1).downto(lim)becomesfor (var i=num; i>=lim; i-=1).step(lim, n).eachbecomesfor (var i=num; i<=lim; i+=n).step(lim, -n).eachbecomesfor (var i=num; i>=lim; i-=n).stripbecomes.trim.subbecomes.replace.to_fbecomesparseFloat.to_ibecomesparseInt.to_sbecomes.to_String.upcasebecomes.toUpperCase[-n]becomes[*.length-n]for literal values ofn[n...m]becomes.slice(n,m)[n..m]becomes.slice(n,m+1)[/r/, n]becomes.match(/r/)[n](1..2).each {|i| ...}becomesfor (var i=1 i<=2; i+=1)"string" * lengthbecomesnew Array(length + 1).join("string").sub!and.gsub!become equivalentx = x.replacestatements.map!,.reverse!, and.selectbecome equivalent.splice(0, .length, *.method())statements@foo.call(args)becomesthis._foo(args)@@foo.call(args)becomesthis.constructor._foo(args)Array(x)becomesArray.prototype.slice.call(x)delete xbecomesdelete x(note lack of parenthesis)setIntervalandsetTimeoutallow block to be treated as the first parameter on the call- for the following methods, if the block consists entirely of a simple
expression (or ends with one), a
returnis added prior to the expression:sub,gsub,any?,all?,map,find,find_index. - New classes subclassed off of
Exceptionwill become subclassed off ofErrorinstead; and default constructors will be provided loop do...endwill be replaced withwhile (true) {...}
Additionally, there is one mapping that will only be done if explicitly included:
.classbecomes.constructor
-
.at()becomes_a.at().between?()becomesR().between().capitalize()becomes_s.capitalize().center()becomes_s.center().chomp()becomes_s.chomp().collect_concat()becomes_e.collect_concat().compact()becomes_a.compact().compact!()becomes_a.compact_bang().count()becomes_e.count().cycle()becomes_e.cycle().delete_at()becomes_a.delete_at().delete_if()becomes_a.delete_if().drop_while()becomes_e.drop_while().each_index()becomes_e.each_index().each_slice()becomes_e.each_slice().each_with_index()becomes_e.each_with_index().each_with_object()becomes_e.each_with_object().find_all()becomes_e.find_all().find()becomes_e.find().flat_map()becomes_e.flat_map().flatten()becomes_a.flatten().grep()becomes_e.grep().group_by()becomes_e.group_by().inject()becomes_e.inject().insert()becomes_a.insert().keep_if()becomes_a.keep_if().ljust()becomes_s.ljust().lstrip()becomes_s.lstrip().map()becomes_e.map().max_by()becomes_e.max_by().min_by()becomes_e.min_by().one?()becomes_e.one().partition()becomes_e.partition().reject()becomes_e.reject().reverse()becomes_a.reverse().reverse!()becomes_a.reverse_bang().reverse_each()becomes_e.reverse_each().rindex()becomes_s.rindex().rjust()becomes_s.rjust().rotate()becomes_a.rotate().rotate!()becomes_a.rotate_bang().rstrip()becomes_s.rstrip().scan()becomes_s.scan().select()becomes_a.select().shift()becomes_a.shift().shuffle()becomes_a.shuffle().shuffle!()becomes_a.shuffle_bang().slice()becomes_a.slice().slice!()becomes_a.slice_bang().sort_by()becomes_e.sort_by().strftime()becomes_t.strftime().swapcase()becomes_s.swapcase().take_while()becomes_e.take_while().transpose()becomes_a.transpose().tr()becomes_s.tr().union()becomes_a.union().uniq()becomes_a.uniq().uniq!()becomes_a.uniq_bang()<=>becomesR.Comparable.cmp()(n..m)becomesR.Range.new()
-
.clone()becomes_.clone().compact()becomes_.compact().count_by {}becomes_.countBy {}.find {}becomes_.find {}.find_by()becomes_.findWhere().flatten()becomes_.flatten().group_by {}becomes_.groupBy {}.has_key?()becomes_.has().index_by {}becomes_.indexBy {}.invert()becomes_.invert().invoke(&:n)becomes_.invoke(, :n).map(&:n)becomes_.pluck(, :n).merge!()becomes_.extend().merge()becomes_.extend({}, ).reduce {}becomes_.reduce {}.reduce()becomes_.reduce().reject {}becomes_.reject {}.sample()becomes_.sample().select {}becomes_.select {}.shuffle()becomes_.shuffle().size()becomes_.size().sort()becomes_.sort_by(, _.identity).sort_by {}becomes_.sortBy {}.times {}becomes_.times {}.values()becomes_.values().where()becomes_.where().zip()becomes_.zip()(n...m)becomes_.range(n, m)(n..m)becomes_.range(n, m+1).compact!,.flatten!,shuffle!,reject!,sort_by!, and.uniqbecome equivalent.splice(0, .length, *.method())statements- for the following methods, if the block consists entirely of a simple
expression (or ends with one), a
returnis added prior to the expression:reduce,sort_by,group_by,index_by,count_by,find,select,reject. is_a?andkind_of?map toObject.prototype.toString.call() === "[object #{type}]" for the following types:Arguments,Boolean,Date,Error,Function,Number,Object,RegExp,String; and maps Ruby names to JavaScript equivalents forException,Float,Hash,Proc, andRegexp. Additionally,is_a?andkind_of?map toArray.isArray()forArray`.
-
- maps Ruby unary operator
~to jQuery$function - maps Ruby attribute syntax to jquery attribute syntax
.to_abecomestoArray- maps
$$to jQuery$function - defaults the fourth parameter of $$.post to
"json", allowing Ruby block syntax to be used for the success function.
- maps Ruby unary operator
-
- maps subclasses of
Minitest::Testtodescribecalls - maps
test_methods inside subclasses ofMinitest::Testtoitcalls - maps
setup,teardown,before, andaftercalls tobeforeEachandafterEachcalls - maps
assertandrefutecalls toexpect...toBeTruthy()andtoBeFalsycalls - maps
assert_equal,refute_equal,.must_equaland.cant_equalcalls toexpect...toBe()calls - maps
assert_in_delta,refute_in_delta,.must_be_within_delta,.must_be_close_to,.cant_be_within_delta, and.cant_be_close_tocalls toexpect...toBeCloseTo()calls - maps
assert_includes,refute_includes,.must_include, and.cant_includecalls toexpect...toContain()calls - maps
assert_match,refute_match,.must_match, and.cant_matchcalls toexpect...toMatch()calls - maps
assert_nil,refute_nil,.must_be_nil, and.cant_be_nillcalls toexpect...toBeNull()calls - maps
assert_operator,refute_operator,.must_be, and.cant_becalls toexpect...toBeGreaterThan()ortoBeLessThancalls
- maps subclasses of
Wunderbar includes additional demos:
When option eslevel: 2015 is provided, the following additional
conversions are made:
"#{a}"becomes`${a}`a = 1becomeslet a = 1A = 1becomesconst A = 1a, b = b, abecomes[a, b] = [b, a]a, (foo, *bar) = xbecomeslet [a, [foo, ...bar]] = xdef f(a, (foo, *bar))becomesfunction f(a, [foo, ...bar])def a(b=1)becomesfunction a(b=1)def a(*b)becomesfunction a(...b).each_keybecomesfor (i of ...) {}a(*b)becomesa(...b)"#{a}"becomes`${a}`lambda {|x| x}becomes(x) => {return x}proc {|x| x}becomes(x) => {x}a {|x|}becomesa((x) => {})class Person; endbecomesclass Person {}
ES2015 class support includes constructors, super, methods, class methods, instance methods, instance variables, class variables, getters, setters, attr_accessor, attr_reader, attr_writer, etc.
Additionally, the functions filter will provide the following conversion:
Array(x)becomesArray.from(x).inject(n) {}becomes.reduce(() => {}, n)
When option eslevel: 2016 is provided, the following additional
conversion is made:
a ** bbecomesa ** b
When option eslevel: 2017 is provided, the following additional
conversion is made by the functions filter:
.each_entrybecomesObject.entries().forEach
dsl — A domain specific language, where code is written in one language and errors are given in another. -- Devil’s Dictionary of Programming
If you simply want to get a job done, and would like a mature and tested framework, and only use one of the many integrations that Opal provides, then Opal is the way to go right now.
ruby2js is for those that want to produce JavaScript that looks like it wasn’t machine generated, and want the absolute bare minimum in terms of limitations as to what JavaScript can be produced.
And, of course, the right solution might be to use CoffeeScript instead.
(The MIT License)
Copyright (c) 2009, 2013 Macario Ortega, Sam Ruby
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.