class Ruby2JS::Converter

Constants

EXPRESSIONS
GROUP_OPERATORS
INVERT_OP
LOGICAL
OPERATORS
VASGN

Attributes

ast[RW]
binding[RW]
comparison[RW]
eslevel[RW]
ivars[RW]
module_type[RW]
namespace[RW]
or[RW]
strict[RW]
underscored_private[RW]

Public Class Methods

handle(*types, &block) click to toggle source
# File lib/ruby2js/converter.rb, line 170
def self.handle(*types, &block)
  types.each do |type| 
    define_method("on_#{type}", block)
    @@handlers << type
  end
end
new( ast, comments, vars = {} ) click to toggle source
Calls superclass method
# File lib/ruby2js/converter.rb, line 39
def initialize( ast, comments, vars = {} )
  super()

  @ast, @comments, @vars = ast, comments, vars.dup
  @varstack = []
  @scope = ast
  @inner = nil
  @rbstack = []
  @next_token = :return

  @handlers = {}
  @@handlers.each do |name|
    @handlers[name] = method("on_#{name}")
  end

  @state = nil
  @block_this = nil
  @block_depth = nil
  @prop = nil
  @instance_method = nil
  @prototype = nil
  @class_parent = nil
  @class_name = nil
  @jsx = false
  @autobind = true

  @eslevel = :es5
  @strict = false
  @comparison = :equality
  @or = :logical
  @underscored_private = true
  @redoable = false
end

Public Instance Methods

collapse_strings(node) click to toggle source

do string concatenation when possible

# File lib/ruby2js/converter/send.rb, line 423
def collapse_strings(node)
  left = node.children[0]
  return node unless left
  right = node.children[2]

  # recursively evaluate left hand side
  if \
    left.type == :send and left.children.length == 3 and
    left.children[1] == :+
  then
    left = collapse_strings(left)
  end

  # recursively evaluate right hand side
  if \
    right.type == :send and right.children.length == 3 and
    right.children[1] == :+
  then
    right = collapse_strings(right)
  end

  # if left and right are both strings, perform concatenation
  if [:dstr, :str].include? left.type and [:dstr, :str].include? right.type
    if left.type == :str and right.type == :str
      return left.updated nil,
        [left.children.first + right.children.first]
    else
      left = s(:dstr, left) if left.type == :str
      right = s(:dstr, right) if right.type == :str
      return left.updated(nil, left.children + right.children)
    end
  end

  # if left and right are unchanged, return original node; otherwise
  # return node modified to include new left and/or right hand sides.
  if left == node.children[0] and right == node.children[2]
    return node
  else
    return node.updated(nil, [left, :+, right])
  end
end
combine_properties(body) click to toggle source
# File lib/ruby2js/converter/begin.rb, line 39
def combine_properties(body)
  (0...body.length-1).each do |i|
    next unless body[i] and body[i].type == :prop
    (i+1...body.length).each do |j|
      break unless body[j] and body[j].type == :prop

      if body[i].children[0] == body[j].children[0]
        # relocate property comment to first method
        [body[i], body[j]].each do |node|
          unless @comments[node].empty?
            node.children[1].values.first.each do |key, value| 
              if [:get, :set].include? key and Parser::AST::Node === value
                @comments[value] = @comments[node]
                break
              end
            end
          end
        end

        # merge properties
        merge = Hash[(body[i].children[1].to_a+body[j].children[1].to_a).
          group_by {|name, value| name.to_s}.map {|name, values|
          [name, values.map(&:last).reduce(:merge)]}]
        body[j] = s(:prop, body[j].children[0], merge)
        body[i] = nil
        break
      end
    end
  end
end
comments(ast) click to toggle source

extract comments that either precede or are included in the node. remove from the list this node may appear later in the tree.

# File lib/ruby2js/converter.rb, line 179
def comments(ast)
  if ast.loc and ast.loc.respond_to? :expression
    expression = ast.loc.expression

    list = @comments[ast].select do |comment|
      expression.source_buffer == comment.loc.expression.source_buffer and
      comment.loc.expression.begin_pos < expression.end_pos
    end
  else
    list = @comments[ast]
  end

  @comments[ast] -= list

  list.map do |comment|
    if comment.text.start_with? '=begin'
      if comment.text.include? '*/'
        comment.text.sub(/\A=begin/, '').sub(/^=end\Z/, '').gsub(/^/, '//')
      else
        comment.text.sub(/\A=begin/, '/*').sub(/^=end\Z/, '*/')
      end
    else
      comment.text.sub(/^#/, '//') + "\n"
    end
  end
end
conditionally_equals(left, right) click to toggle source

determine if two trees are identical, modulo conditionalilties in other words a.b == a&.b

# File lib/ruby2js/converter/logical.rb, line 89
def conditionally_equals(left, right)
  if left == right
    true
  elsif !left.respond_to?(:type) or !left or !right or left.type != :csend or right.type != :send
    false
  else
    conditionally_equals(left.children.first, right.children.first) &&
      conditionally_equals(left.children.last, right.children.last)
  end
end
convert() click to toggle source
# File lib/ruby2js/converter.rb, line 77
def convert
  scope @ast 

  if @strict
    if @sep == '; '
      @lines.first.unshift "\"use strict\"#@sep"
    else
      @lines.unshift Line.new('"use strict";')
    end
  end
end
es2015() click to toggle source
# File lib/ruby2js/converter.rb, line 137
def es2015
  @eslevel >= 2015
end
es2016() click to toggle source
# File lib/ruby2js/converter.rb, line 141
def es2016
  @eslevel >= 2016
end
es2017() click to toggle source
# File lib/ruby2js/converter.rb, line 145
def es2017
  @eslevel >= 2017
end
es2018() click to toggle source
# File lib/ruby2js/converter.rb, line 149
def es2018
  @eslevel >= 2018
end
es2019() click to toggle source
# File lib/ruby2js/converter.rb, line 153
def es2019
  @eslevel >= 2019
end
es2020() click to toggle source
# File lib/ruby2js/converter.rb, line 157
def es2020
  @eslevel >= 2020
end
es2021() click to toggle source
# File lib/ruby2js/converter.rb, line 161
def es2021
  @eslevel >= 2021
end
es2022() click to toggle source
# File lib/ruby2js/converter.rb, line 165
def es2022
  @eslevel >= 2022
end
group( ast ) click to toggle source
# File lib/ruby2js/converter.rb, line 240
def group( ast )
  if [:dstr, :dsym].include? ast.type and es2015
    parse ast
  else
    put '('; parse ast; put ')'
  end
end
hoist?(outer, inner, name) click to toggle source

is ‘name’ referenced outside of inner scope?

# File lib/ruby2js/converter/vasgn.rb, line 68
def hoist?(outer, inner, name)
  outer.children.each do |var|
    next if var == inner
    return true if var == name and [:lvar, :gvar].include? outer.type
    return true if Parser::AST::Node === var and hoist?(var, inner, name)
  end
  return false
end
jscope( ast, args=nil ) click to toggle source

handle the oddity where javascript considers there to be a scope (e.g. the body of an if statement), whereas Ruby does not.

# File lib/ruby2js/converter.rb, line 119
def jscope( ast, args=nil )
  @varstack.push @vars
  @vars = args if args
  @vars = Hash[@vars.map {|key, value| [key, true]}]

  parse( ast, :statement )
ensure
  pending = @vars.select {|key, value| value == :pending}
  @vars = @varstack.pop
  @vars.merge! pending
end
multi_assign_declarations() click to toggle source
# File lib/ruby2js/converter/vasgn.rb, line 77
def multi_assign_declarations
  undecls = []
  child = @ast
  loop do
    if [:send, :casgn].include? child.type
      subchild = child.children[2]
    else
      subchild = child.children[1]
    end

    if subchild.type == :send
      break unless subchild.children[1] =~ /=$/
    else
      break unless [:send, :cvasgn, :ivasgn, :gvasgn, :lvasgn].
        include? subchild.type
    end

    child = subchild

    if child.type == :lvasgn and not @vars.include?(child.children[0]) 
      undecls << child.children[0]
    end
  end

  unless undecls.empty?
    if es2015
      put "let "
    else
      put "var "
    end
    put "#{undecls.map(&:to_s).join(', ')}#@sep"
  end
end
number_format(number) click to toggle source
# File lib/ruby2js/converter/literal.rb, line 20
def number_format(number)
  return number.to_s unless es2021
  parts = number.to_s.split('.')
  parts[0] = parts[0].gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1_")
  parts[1] = parts[1].gsub(/(\d\d\d)(?=\d)/, "\\1_") if parts[1]
  parts.join('.')
end
operator_index(op) click to toggle source
# File lib/ruby2js/converter.rb, line 89
def operator_index op
  OPERATORS.index( OPERATORS.find{ |el| el.include? op } ) || -1
end
parse(ast, state=:expression) click to toggle source
# File lib/ruby2js/converter.rb, line 206
def parse(ast, state=:expression)
  oldstate, @state = @state, state
  oldast, @ast = @ast, ast
  return unless ast

  handler = @handlers[ast.type]

  unless handler
    raise Error.new("unknown AST type #{ ast.type }", ast)
  end

  if state == :statement and not @comments[ast].empty?
    comments(ast).each {|comment| puts comment.chomp}
  end

  handler.call(*ast.children)
ensure
  @ast = oldast
  @state = oldstate
end
parse_all(*args) click to toggle source
# File lib/ruby2js/converter.rb, line 227
def parse_all(*args)
  @options = (Hash === args.last) ? args.pop : {}
  sep = @options[:join].to_s
  state = @options[:state] || :expression

  index = 0
  args.each do |arg|
    put sep unless index == 0
    parse arg, state
    index += 1 unless arg == s(:begin)
  end
end
range_to_array(node) click to toggle source
# File lib/ruby2js/converter/send.rb, line 465
def range_to_array(node)
  start, finish = node.children
  if start.type == :int and start.children.first == 0
    # Ranges which start from 0 can be achieved with more simpler code
    if finish.type == :int
      # output cleaner code if we know the value already
      length = finish.children.first + (node.type == :irange ? 1 : 0)
    else
      # If this is variable we need to fix indexing by 1 in js
      length = "#{finish.children.last}" + (node.type == :irange ? "+1" : "")
    end

    if es2015
      return put "[...Array(#{length}).keys()]"
    else
      return put "Array.apply(null, {length: #{length}}).map(Function.call, Number)"
    end
  else
    # Use .compact because the first argument is nil with variables
    # This way the first value is always set
    start_value = start.children.compact.first
    finish_value = finish.children.compact.first
    if start.type == :int and finish.type == :int
      length = finish_value - start_value + (node.type == :irange ? 1 : 0)
    else
      length = "(#{finish_value}-#{start_value}" + (node.type == :irange ? "+1" : "") + ")"
    end

    # Avoid of using same variables in the map as used in the irange or elsewhere in this code
    # Ruby2js only allows dollar sign in beginning of variable so i$ is safe
    if @vars.include? :idx or start_value == :idx or finish_value == :idx
      index_var = 'i$'
    else
      index_var = 'idx'
    end

    if es2015
      # Use _ because it's normal convention in JS for variable which is not used at all
      if @vars.include? :_ or start_value == :_ or finish_value == :_
        blank = '_$'
      else
        blank = '_'
      end

      return put "Array.from({length: #{length}}, (#{blank}, #{index_var}) => #{index_var}+#{start_value})"
    else
      return put "Array.apply(null, {length: #{length}}).map(Function.call, Number).map(function (#{index_var}) { return #{index_var}+#{start_value} })"
    end
  end
end
redoable(block) click to toggle source
# File lib/ruby2js/converter.rb, line 248
def redoable(block)
  save_redoable = @redoable

  has_redo = proc do |node|
    node.children.any? do |child|
      next false unless child.is_a? Parser::AST::Node
      next true if child.type == :redo
      next false if %i[for while while_post until until_post].include? child.type
      has_redo[child]
    end
  end

  @redoable = has_redo[@ast]

  if @redoable
    put es2015 ? 'let ' : 'var '
    put "redo$#@sep"
    puts 'do {'
    put "redo$ = false#@sep"
    scope block
    put "#@nl} while(redo$)"
  else
    scope block
  end
ensure
  @redoable = save_redoable
end
rewrite(left, right) click to toggle source

rewrite a && a.b to a&.b

# File lib/ruby2js/converter/logical.rb, line 67
def rewrite(left, right)
  if left && left.type == :and
    left = rewrite(*left.children)
  end

  if right.type != :send or OPERATORS.flatten.include? right.children[1]
    s(:and, left, right)
  elsif conditionally_equals(left, right.children.first)
    # a && a.b => a&.b
    right.updated(:csend, [left, *right.children[1..-1]])
  elsif conditionally_equals(left.children.last, right.children.first)
    # a && b && b.c => a && b&.c
    left.updated(:and, [left.children.first,
      left.children.last.updated(:csend, 
      [left.children.last, *right.children[1..-1]])])
  else
    s(:and, left, right)
  end
end
s(type, *args) click to toggle source
# File lib/ruby2js/converter.rb, line 131
def s(type, *args)
  Parser::AST::Node.new(type, args)
end
scope( ast, args=nil ) click to toggle source

define a new scope; primarily determines what variables are visible and deals with hoisting of declarations

# File lib/ruby2js/converter.rb, line 95
def scope( ast, args=nil )
  scope, @scope = @scope, ast
  inner, @inner = @inner, nil 
  mark = output_location
  @varstack.push @vars
  @vars = args if args
  @vars = Hash[@vars.map {|key, value| [key, true]}]

  parse( ast, :statement )

  # retroactively add a declaration for 'pending' variables
  vars = @vars.select {|key, value| value == :pending}.keys
  unless vars.empty?
    insert mark, "#{es2015 ? 'let' : 'var'} #{vars.join(', ')}#{@sep}"
    vars.each {|var| @vars[var] = true}
  end
ensure
  @vars = @varstack.pop
  @scope = scope
  @inner = inner
end
timestamp(file) click to toggle source
Calls superclass method
# File lib/ruby2js/converter.rb, line 276
def timestamp(file)
  super

  return unless file

  walk = proc do |ast|
    if ast.loc and ast.loc.expression
      filename = ast.loc.expression.source_buffer.name
      if filename and not filename.empty?
        @timestamps[filename] ||= File.mtime(filename) rescue nil
      end
    end

    ast.children.each do |child|
      walk[child] if child.is_a? Parser::AST::Node
    end
  end

  walk[@ast] if @ast
end
transform_defs(target, method, args, body) click to toggle source
# File lib/ruby2js/converter/defs.rb, line 21
def transform_defs(target, method, args, body)
  if not @ast.is_method? or @ast.type == :defp
    node = s(:prop, target, method.to_s =>
      {enumerable: s(:true), configurable: s(:true),
      get: s(:block, s(:send, nil, :proc), args,
      s(:autoreturn, body))})
  elsif method =~ /=$/
    node = s(:prop, target, method.to_s.sub('=', '') =>
      {enumerable: s(:true), configurable: s(:true),
      set: s(:block, s(:send, nil, :proc), args,
      body)})
  else
    node = s(:send, target, "#{method}=", s(:def, nil, args, body))
  end

  @comments[node] = @comments[@ast] if @comments[@ast]

  node
end
width=(width) click to toggle source
# File lib/ruby2js/converter.rb, line 73
def width=(width)
  @width = width
end