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