Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Ruby to JavaScript conversion

License

NotificationsYou must be signed in to change notification settings

ruby2js/ruby2js

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Minimal yet extensible Ruby to JavaScript conversion.

Build StatusGem VersionGitter

Description

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 IFthere are either one or more arguments passed ORparenthesis are used
  • otherwise Ruby method calls become JavaScript property accesses.
  • by default, methods and procs returnundefined
  • splats mapped to spread syntax when ES2015 or later is selected, andto equivalents usingapply,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 argumentnew Promise do; y(); end becomesnew Promise(function() {y()})
  • regular expressions are mapped to js
  • raise becomesthrow
  • expressions enclosed in backtick operators (``) and%x{} literals areevaluated in the context of the caller and the results are insertedinto the generated JavaScript.

Ruby attribute accessors, methods defined with no parameters and noparenthesis, as well as setter method definitions, aremapped toObject.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 fordefining and extending an existing class, whereas JavaScript does not. Thismeans that Ruby2JS needs to be told when a class is being extended, which isdone by prepending theclass keyword with two plus signs, thus:++class C; ...; end.

Filters may be provided to add Ruby-specific or framework specificbehavior. Filters are essentially macro facilities that operate onan AST representation of the code.

Seenotimplemented_specfor a list of Ruby featuresknown to be not implemented.

Synopsis

Basic:

require'ruby2js'putsRuby2JS.convert("a={age:3}\na.age+=1")

With filter:

require'ruby2js/filter/functions'putsRuby2JS.convert('"2A".to_i(16)')

Enable ES2015 support:

putsRuby2JS.convert('"#{a}"',eslevel:2015)

Enable strict support:

putsRuby2JS.convert('a=1',strict:true)

Emit strict equality comparisons:

putsRuby2JS.convert('a==1',comparison::identity)

Emit nullish coalescing operators:

putsRuby2JS.convert('a || 1',or::nullish)

WithExecJS:

require'ruby2js/execjs'require'date'context=Ruby2JS.compile(Date.today.strftime('d = new Date(%Y, %-m-1, %-d)'))putscontext.eval('d.getYear()')+1900

Conversions can be explored interactively using thedemo provided.

Introduction

JavaScript is a language where0 is consideredfalse, strings areimmutable, and the behaviors for operators like== are, at best,convoluted.

Any attempt to bridge the semantics of Ruby and JavaScript will involvetrade-offs. Consider the following expression:

a[-1]

Programmers who are familiar with Ruby will recognize that this returns thelast element (or character) of an array (or string). However, the meaning isquite different ifa 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 JavaScriptobjects to handle this. This is the approach thatOpaltakes. It is a fine approach, with a number of benefits. It also has somenotable drawbacks. For example,readabilityandcompatibility with other frameworks.

Another approach is to simply accept JavaScript semantics for what they are.This would mean that negative indexes would returnundefined for arraysand strings. This is the base approach provided by ruby2js.

A third approach would be to do static transformations on the source in orderto address common usage patterns or idioms. These transformations can even beoccasionally unsafe, as long as the transformations themselves are opt-in.ruby2js provides a number of such filters, including one that handles negativeindexes when passed as a literal. As indicated above, this is unsafe in thatit will do the wrong thing when it encounters a hash index which is expressedas a literal constant negative one. My experience is that such is rare enoughto be safely ignored, but YMMV. More troublesome, this also won’t work whenthe index is not a literal (e.g.,a[n]) and the index happens to benegative at runtime.

This quickly gets into gray areas.each in Ruby is a common method thatfacilitates iteration over arrays.forEach is the JavaScript equivalent.Mapping this is fine until you start using a framework like jQuery whichprovides a function namedeach.

Fortunately, Ruby provides? and! as legal suffixes for method names,Ruby2js filters do an exact match, so if you select a filter that mapseachtoforEach,each! will pass through the filter. The final code that emitsJavaScript function calls and parameter accesses will strip off thesesuffixes.

This approach works well if it is an occasional change, but if the usage ispervasive, most filters support options toexclude a list of mappings,for example:

putsRuby2JS.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 beforewarned, that implementing a function likemethod_missing would require alot of work.

Integrations

While this is a low level library suitable for DIY integration, one of theobvious uses of a tool that produces JavaScript is by web servers. Ruby2JSincludes three such integrations:

As you might expect, CGI is a bit sluggish. By contrast, Sinatra and Railsare quite speedy as the bulk of the time is spent on the initial load of therequired libraries.

Filters

In general, making use of a filter is as simple as requiring it. If multiplefilters are selected, they will all be applied in parallel in one pass throughthe script.

  • returnaddsreturn to the last expression in functions.

  • requiresupportsrequire andrequire_relative statements. Contents of filesthat are required are converted to JavaScript and expanded inline.require function calls in expressions are left alone.

  • camelCaseconvertsunderscore_case tocamelCase. Seecamelcase_specfor examples.

  • functions

    • .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 thefirst parameter on the call
    • for the following methods, if the block consists entirely of a simpleexpression (or ends with one), areturn is added prior to theexpression:sub,gsub,any?,all?,map,find,find_index.
    • New classes subclassed off ofException will become subclassed offofError 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 explicitlyincluded (passinclude: :class as aconvert option to enable):

    • .class becomes.constructor
  • tagged_templates

    Allows you to turn certain method calls with a string argument into taggedtemplate literals. By default it supports html and css, so you can writehtml "<div>#{1+2}</div>" which converts tohtml`<div>${1+2}</div>`.Works nicely with squiggly heredocs for multi-line templates as well. If youneed to configure the tag names yourself, pass atemplate_literal_tagsoption toconvert with an array of tag name symbols.

    Note: these conversions are only done if eslevel >= 2015

  • esm

    Provides conversion of import and export statements for use with modern ES builders like Webpack.

    Examples:

    import

    import"./index.scss"# => import "./index.scss"importSomethingfrom"./lib/something"# => import Something from "./lib/something"importSomething,"./lib/something"# => import Something from "./lib/something"import[LitElement,html,css],from:"lit-element"# => import { LitElement, html, css } from "lit-element"importReact,from:"react"# => import React from "react"importReact,as:"*",from:"react"# => import React as * from "react"

    export

    exporthash={ab:123}# => export const hash = {ab: 123};exportfunc=->(x){x *10}# => export const func = x => x * 10;exportdefmultiply(x,y)returnx *yend# => export function multiply(x, y) {#      return x * y#    }exportdefaultclassMyClassend# => export default class MyClass {#    };# or final export statement:export[one,two,default:three]# => export { one, two, three as default }
  • node

    • `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"})
  • nokogiri

    • 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
  • underscore

    • .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 simpleexpression (or ends with one), areturn is added prior to theexpression: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`.
  • jquery

    • 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 blocksyntax to be used for the success function.
  • minitest-jasmine

    • maps subclasses ofMinitest::Test todescribe calls
    • mapstest_ methods inside subclasses ofMinitest::Test toit calls
    • mapssetup,teardown,before, andafter calls tobeforeEachandafterEach calls
    • mapsassert andrefute calls toexpect...toBeTruthy() andtoBeFalsy calls
    • mapsassert_equal,refute_equal,.must_equal and.cant_equalcalls toexpect...toBe() calls
    • mapsassert_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
    • mapsassert_includes,refute_includes,.must_include, and.cant_include calls toexpect...toContain() calls
    • mapsassert_match,refute_match,.must_match, and.cant_matchcalls toexpect...toMatch() calls
    • mapsassert_nil,refute_nil,.must_be_nil, and.cant_be_nill callstoexpect...toBeNull() calls
    • mapsassert_operator,refute_operator,.must_be, and.cant_becalls toexpect...toBeGreaterThan() ortoBeLessThan calls
  • cjs

    • mapsexport def f toexports.f =
    • mapsexport async def f toexports.f = async
    • mapsexport v = toexports.v =
    • mapsexport default proc tomodule.exports =
    • mapsexport default async proc tomodule.exports = async
    • mapsexport default tomodule.exports =
  • matchAll

    For ES level < 2020:

    • mapsstr.matchAll(pattern).forEach {} towhile (match = pattern.exec(str)) {}

    Notepattern must be a simple variable with a value of a regularexpression with theg flag set at runtime.

Wunderbar includes additional demos:

ES2015 support

When optioneslevel: 2015 is provided, the following additionalconversions 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, thefunctions 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 toparameter detructuring.

ES2016 support

When optioneslevel: 2016 is provided, the following additionalconversion is made:

  • a ** b becomesa ** b
  • .include? becomes.includes

ES2017 support

When optioneslevel: 2017 is provided, the following additionalconversions are made by thefunctions filter:

  • .values() becomesObject.values()
  • .entries() becomesObject.entries()
  • .each_pair {} becomes `for (let [key, value] of Object.entries()) {}'

ES2018 support

When optioneslevel: 2018 is provided, the following additionalconversion is made by thefunctions filter:

  • .merge becomes{...a, ...b}

Additionally, rest arguments can now be used with keyword arguments andoptional keyword arguments.

ES2019 support

When optioneslevel: 2019 is provided, the following additionalconversion is made by thefunctions filter:

  • .flatten becomes.flat(Infinity)
  • .lstrip becomes `.trimEnd
  • .rstrip becomes `.trimStart
  • a.to_h becomesObject.fromEntries(a)
  • Hash[a] becomesObject.fromEntries(a)

Additionally,rescue without a variable will map tocatch without avariable.

ES2020 support

When optioneslevel: 2020 is provided, the following additionalconversions are made:

  • @x becomesthis.#x
  • @@x becomesClassName.#x
  • a&.b becomesa?.b
  • .scan becomesArray.from(str.matchAll(/.../g), s => s.slice(1))

ES2021 support

When optioneslevel: 2021 is provided, the following additionalconversions are made:

  • x ||= 1 becomesx ||= 1
  • x &&= 1 becomesx &&= 1

Picking a Ruby to JS mapping tool

dsl — A domain specific language, where code is written in one language anderrors are given in another.--Devil’s Dictionary of Programming

If you simply want to get a job done, and would like a mature and testedframework, and only use one of the many integrations thatOpal provides, then Opal is the way to go right now.

ruby2js is for those that want to produce JavaScript that looks like itwasn’t machine generated, and want the absolute bare minimum in terms oflimitations as to what JavaScript can be produced.

Try for yourself.Compare.

And, of course, the right solution might be to useCoffeeScript instead.

License

(The MIT License)

Copyright (c) 2009, 2013 Macario Ortega, Sam Ruby

Permission is hereby granted, free of charge, to any person obtaininga copy of this software and associated documentation files (the'Software'), to deal in the Software without restriction, includingwithout limitation the rights to use, copy, modify, merge, publish,distribute, sublicense, and/or sell copies of the Software, and topermit persons to whom the Software is furnished to do so, subject tothe following conditions:

The above copyright notice and this permission notice shall beincluded 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 OFMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANYCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THESOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

Ruby to JavaScript conversion

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors15


[8]ページ先頭

©2009-2025 Movatter.jp