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
, andarguments
otherwise. - ruby string interpolation is expanded into string + operations
and
andor
become&&
and||
a ** b
becomesMath.pow(a,b)
<< a
becomes.push(a)
unless
becomesif !
until
becomeswhile !
case
andwhen
becomesswitch
andcase
- 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; end
becomesfunction Person() {}
- instance methods become prototype methods
- instance variables become underscored,
@name
becomesthis._name
- self is assigned to this is if used
- Any block becomes and explicit argument
new Promise do; y(); end
becomesnew Promise(function() {y()})
- regular expressions are mapped to js
raise
becomesthrow
- 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)
Emit strict equality comparisons:
puts Ruby2JS.convert('a==1', comparison: :identity)
Emit nullish coalescing operators:
puts Ruby2JS.convert('a || 1', or: :nullish)
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()')+1900
Conversions 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 :each
Static 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
return
to the last expression in functions. -
require supports
require
andrequire_relative
statements. Contents of files that are required are converted to JavaScript and expanded inline.require
function calls in expressions are left alone. -
camelCase converts
underscore_case
tocamelCase
. See camelcase_spec for examples. -
.all?
becomes.every
.any?
becomes.some
.chr
becomesfromCharCode
.clear
becomes.length = 0
.delete
becomesdelete target[arg]
.downcase
becomes.toLowerCase
.each
becomes.forEach
.each_key
becomesfor (i in ...) {}
.each_pair
becomesfor (var key in item) {var value = item[key]; ...}
.each_value
becomes.forEach
.each_with_index
becomes.forEach
.end_with?
becomes.slice(-arg.length) == arg
.empty?
becomes.length == 0
.find_index
becomesfindIndex
.first
becomes[0]
.first(n)
becomes.slice(0, n)
.gsub
becomesreplace(//g)
.include?
becomes.indexOf() != -1
.inspect
becomesJSON.stringify()
.keys()
becomesObject.keys()
.last
becomes[*.length-1]
.last(n)
becomes.slice(*.length-1, *.length)
.lstrip
becomes.replace(/^\s+/, "")
.max
becomesMath.max.apply(Math)
.merge
becomesObject.assign({}, ...)
.merge!
becomesObject.assign()
.min
becomesMath.min.apply(Math)
.nil?
becomes== null
.ord
becomescharCodeAt(0)
puts
becomesconsole.log
.replace
becomes.length = 0; ...push.apply(*)
.respond_to?
becomesright in left
.rstrip
becomes.replace(/s+$/, "")
.scan
becomes.match(//g)
.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).each
becomesfor (var i=num; i<=lim; i+=n)
.step(lim, -n).each
becomesfor (var i=num; i>=lim; i-=n)
(0..a).to_a
becomesArray.apply(null, {length: a}).map(Function.call, Number)
(b..a).to_a
becomesArray.apply(null, {length: (a-b+1)}).map(Function.call, Number).map(function (idx) { return idx+b })
(b...a).to_a
becomesArray.apply(null, {length: (a-b)}).map(Function.call, Number).map(function (idx) { return idx+b })
.strip
becomes.trim
.sub
becomes.replace
.to_f
becomesparseFloat
.to_i
becomesparseInt
.to_s
becomes.to_String
.upcase
becomes.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]
[/r/, n]=
becomes.replace(/r/, ...)
(1..2).each {|i| ...}
becomesfor (var i=1 i<=2; i+=1)
"string" * length
becomesnew Array(length + 1).join("string")
.sub!
and.gsub!
become equivalentx = x.replace
statements.map!
,.reverse!
, and.select
become 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 x
becomesdelete x
(note lack of parenthesis)setInterval
andsetTimeout
allow 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
return
is added prior to the expression:sub
,gsub
,any?
,all?
,map
,find
,find_index
. - New classes subclassed off of
Exception
will become subclassed off ofError
instead; and default constructors will be provided loop do...end
will be replaced withwhile (true) {...}
raise Exception.new(...)
will be replaced withthrow new Error(...)
Additionally, there is one mapping that will only be done if explicitly included (pass
include: :class
as aconvert
option to enable):.class
becomes.constructor
-
Allows you to turn certain method calls with a string argument into tagged template literals. By default it supports html and css, so you can write
html "<div>#{1+2}</div>"
which converts tohtml`<div>${1+2}</div>`
. Works nicely with squiggly heredocs for multi-line templates as well. If you need to configure the tag names yourself, pass atemplate_literal_tags
option toconvert
with an array of tag name symbols.Note: these conversions are only done if eslevel >= 2015
-
Provides conversion of import and export statements for use with modern ES builders like Webpack.
Examples:
import
import "./index.scss" # => import "./index.scss" import Something from "./lib/something" # => import Something from "./lib/something" import Something, "./lib/something" # => import Something from "./lib/something" import [ LitElement, html, css ], from: "lit-element" # => import { LitElement, html, css } from "lit-element" import React, from: "react" # => import React from "react" import React, as: "*", from: "react" # => import React as * from "react"
export
export hash = { ab: 123 } # => export const hash = {ab: 123}; export func = ->(x) { x * 10 } # => export const func = x => x * 10; export def multiply(x, y) return x * y end # => export function multiply(x, y) { # return x * y # } export default class MyClass end # => export default class MyClass { # }; # or final export statement: export [ one, two, default: three ] # => export { one, two, three as default }
-
`command`
becomeschild_process.execSync("command", {encoding: "utf8"})
ARGV
becomesprocess.argv.slice(2)
__dir__
becomes__dirname
Dir.chdir
becomesprocess.chdir
Dir.entries
becomesfs.readdirSync
Dir.mkdir
becomesfs.mkdirSync
Dir.mktmpdir
becomesfs.mkdtempSync
Dir.pwd
becomesprocess.cwd
Dir.rmdir
becomesfs.rmdirSync
ENV
becomesprocess.env
__FILE__
becomes__filename
File.chmod
becomesfs.chmodSync
File.chown
becomesfs.chownSync
File.cp
becomesfs.copyFileSync
File.exist?
becomesfs.existsSync
File.lchmod
becomesfs.lchmodSync
File.link
becomesfs.linkSync
File.ln
becomesfs.linkSync
File.lstat
becomesfs.lstatSync
File.read
becomesfs.readFileSync
File.readlink
becomesfs.readlinkSync
File.realpath
becomesfs.realpathSync
File.rename
becomesfs.renameSync
File.stat
becomesfs.statSync
File.symlink
becomesfs.symlinkSync
File.truncate
becomesfs.truncateSync
File.unlink
becomesfs.unlinkSync
FileUtils.cd
becomesprocess.chdir
FileUtils.cp
becomesfs.copyFileSync
FileUtils.ln
becomesfs.linkSync
FileUtils.ln_s
becomesfs.symlinkSync
FileUtils.mkdir
becomesfs.mkdirSync
FileUtils.mv
becomesfs.renameSync
FileUtils.pwd
becomesprocess.cwd
FileUtils.rm
becomesfs.unlinkSync
IO.read
becomesfs.readFileSync
IO.write
becomesfs.writeFileSync
system
becomeschild_process.execSync(..., {stdio: "inherit"})
-
add_child
becomesappendChild
add_next_sibling
becomesnode.parentNode.insertBefore(sibling, node.nextSibling)
add_previous_sibling
becomesnode.parentNode.insertBefore(sibling, node)
after
becomesnode.parentNode.insertBefore(sibling, node.nextSibling)
at
becomesquerySelector
attr
becomesgetAttribute
attribute
becomesgetAttributeNode
before
becomesnode.parentNode.insertBefore(sibling, node)
cdata?
becomesnode.nodeType === Node.CDATA_SECTION_NODE
children
becomeschildNodes
comment?
becomesnode.nodeType === Node.COMMENT_NODE
content
becomestextContent
create_element
becomescreateElement
document
becomesownerDocument
element?
becomesnode.nodeType === Node.ELEMENT_NODE
fragment?
becomesnode.nodeType === Node.FRAGMENT_NODE
get_attribute
becomesgetAttribute
has_attribute
becomeshasAttribute
inner_html
becomesinnerHTML
key?
becomeshasAttribute
name
becomesnextSibling
next
becomesnodeName
next=
becomesnode.parentNode.insertBefore(sibling,node.nextSibling)
next_element
becomesnextElement
next_sibling
becomesnextSibling
Nokogiri::HTML5
becomesnew JSDOM().window.document
Nokogiri::HTML5.parse
becomesnew JSDOM().window.document
Nokogiri::HTML
becomesnew JSDOM().window.document
Nokogiri::HTML.parse
becomesnew JSDOM().window.document
Nokogiri::XML::Node.new
becomesdocument.createElement()
parent
becomesparentNode
previous=
becomesnode.parentNode.insertBefore(sibling, node)
previous_element
becomespreviousElement
previous_sibling
becomespreviousSibling
processing_instruction?
becomesnode.nodeType === Node.PROCESSING_INSTRUCTION_NODE
remove_attribute
becomesremoveAttribute
root
becomesdocumentElement
search
becomesquerySelectorAll
set_attribute
becomessetAttribute
text?
becomesnode.nodeType === Node.TEXT_NODE
text
becomestextContent
to_html
becomesouterHTML
-
.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.uniq
become equivalent.splice(0, .length, *.method())
statements- for the following methods, if the block consists entirely of a simple
expression (or ends with one), a
return
is 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 for
Exception,
Float,
Hash,
Proc, and
Regexp. Additionally,
is_a?and
kind_of?map to
Array.isArray()for
Array`.
-
- maps Ruby unary operator
~
to jQuery$
function - maps Ruby attribute syntax to jquery attribute syntax
.to_a
becomestoArray
- 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::Test
todescribe
calls - maps
test_
methods inside subclasses ofMinitest::Test
toit
calls - maps
setup
,teardown
,before
, andafter
calls tobeforeEach
andafterEach
calls - maps
assert
andrefute
calls toexpect
...toBeTruthy()
andtoBeFalsy
calls - maps
assert_equal
,refute_equal
,.must_equal
and.cant_equal
calls 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_to
calls toexpect
...toBeCloseTo()
calls - maps
assert_includes
,refute_includes
,.must_include
, and.cant_include
calls toexpect
...toContain()
calls - maps
assert_match
,refute_match
,.must_match
, and.cant_match
calls toexpect
...toMatch()
calls - maps
assert_nil
,refute_nil
,.must_be_nil
, and.cant_be_nill
calls toexpect
...toBeNull()
calls - maps
assert_operator
,refute_operator
,.must_be
, and.cant_be
calls toexpect
...toBeGreaterThan()
ortoBeLessThan
calls
- maps subclasses of
-
- maps
export def f
toexports.f =
- maps
export async def f
toexports.f = async
- maps
export v =
toexports.v =
- maps
export default proc
tomodule.exports =
- maps
export default async proc
tomodule.exports = async
- maps
export default
tomodule.exports =
- maps
-
For ES level < 2020:
- maps
str.matchAll(pattern).forEach {}
towhile (match = pattern.exec(str)) {}
Note
pattern
must be a simple variable with a value of a regular expression with theg
flag set at runtime. - maps
Wunderbar includes additional demos:
When option eslevel: 2015
is provided, the following additional
conversions are made:
"#{a}"
becomes`${a}`
a = 1
becomeslet a = 1
A = 1
becomesconst A = 1
a, b = b, a
becomes[a, b] = [b, a]
a, (foo, *bar) = x
becomeslet [a, [foo, ...bar]] = x
def f(a, (foo, *bar))
becomesfunction f(a, [foo, ...bar])
def a(b=1)
becomesfunction a(b=1)
def a(*b)
becomesfunction a(...b)
.each_value
becomesfor (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; end
becomesclass Person {}
(0...a).to_a
becomes[...Array(a).keys()]
(0..a).to_a
becomes[...Array(a+1).keys()]
(b..a).to_a
becomesArray.from({length: (a-b+1)}, (_, idx) => idx+b)
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)
Finally, keyword arguments and optional keyword arguments will be mapped to parameter detructuring.
When option eslevel: 2016
is provided, the following additional
conversion is made:
a ** b
becomesa ** b
.include?
becomes.includes
When option eslevel: 2017
is provided, the following additional
conversions are made by the functions
filter:
.values()
becomesObject.values()
.entries()
becomesObject.entries()
.each_pair {}
becomes `for (let [key, value] of Object.entries()) {}'
When option eslevel: 2018
is provided, the following additional
conversion is made by the functions
filter:
.merge
becomes{...a, ...b}
Additionally, rest arguments can now be used with keyword arguments and optional keyword arguments.
When option eslevel: 2019
is provided, the following additional
conversion is made by the functions
filter:
.flatten
becomes.flat(Infinity)
.lstrip
becomes `.trimEnd.rstrip
becomes `.trimStarta.to_h
becomesObject.fromEntries(a)
Hash[a]
becomesObject.fromEntries(a)
Additionally, rescue
without a variable will map to catch
without a
variable.
When option eslevel: 2020
is provided, the following additional
conversions are made:
@x
becomesthis.#x
@@x
becomesClassName.#x
a&.b
becomesa?.b
.scan
becomesArray.from(str.matchAll(/.../g), s => s.slice(1))
When option eslevel: 2021
is provided, the following additional
conversions are made:
x ||= 1
becomesx ||= 1
x &&= 1
becomesx &&= 1
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.