Class: Ruby2JS::Converter

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby2js/converter.rb

Constant Summary collapse

LOGICAL =
:and, :not, :or
OPERATORS =
[:[], :[]=], [:not, :!], [:*, :/, :%], [:+, :-], [:>>, :<<], 
[:<=, :<, :>, :>=], [:==, :!=, :===, :"!=="], [:and, :or]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ast, vars = {}) ⇒ Converter

Returns a new instance of Converter.



11
12
13
14
15
16
17
# File 'lib/ruby2js/converter.rb', line 11

def initialize( ast, vars = {} )
  @ast, @vars = ast, vars.dup
  @sep = '; '
  @nl = ''
  @ws = ' '
  @varstack = []
end

Instance Attribute Details

#bindingObject

Returns the value of attribute binding.



9
10
11
# File 'lib/ruby2js/converter.rb', line 9

def binding
  @binding
end

Instance Method Details

#enable_vertical_whitespaceObject



19
20
21
22
23
# File 'lib/ruby2js/converter.rb', line 19

def enable_vertical_whitespace
  @sep = ";\n"
  @nl = "\n"
  @ws = @nl
end

#group(ast) ⇒ Object



535
536
537
# File 'lib/ruby2js/converter.rb', line 535

def group( ast )
  "(#{ parse ast })"
end

#is_method?(node) ⇒ Boolean

Returns:

  • (Boolean)


49
50
51
52
53
54
55
# File 'lib/ruby2js/converter.rb', line 49

def is_method?(node)
  return false unless node.type == :send
  return true unless node.loc
  selector = node.loc.selector
  return true unless selector.source_buffer
  selector.source_buffer.source[selector.end_pos] == '('
end

#operator_index(op) ⇒ Object



33
34
35
# File 'lib/ruby2js/converter.rb', line 33

def operator_index op
  OPERATORS.index( OPERATORS.find{ |el| el.include? op } ) || -1
end

#parse(ast, state = :expression) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
# File 'lib/ruby2js/converter.rb', line 57

def parse(ast, state=:expression)
  return ast unless Parser::AST::Node === ast

  case ast.type
    
  when :int, :float, :str
    ast.children.first.inspect

  when :sym
    ast.children.first.to_s.inspect

  when :lvar, :gvar, :cvar
    ast.children.first
    
  when :true, :false
    ast.type.to_s
    
  when :nil
    'null'

  when :lvasgn
    var, value = ast.children
    output      = value ? "#{ 'var ' unless @vars.keys.include? var }#{ var } = #{ parse value }" : var
    @vars[var] = true
    output

  when :op_asgn
    var, op, value = ast.children

    if [:+, :-].include?(op) and value.type==:int and value.children==[1]
      if state == :statement
        "#{ parse var }#{ op }#{ op }"
      else
        "#{ op }#{ op }#{ parse var }"
      end
    else
      "#{ parse var } #{ op }= #{ parse value }"
    end

  when :casgn
    cbase, var, value = ast.children
    var = "#{cbase}.var" if cbase
    output = "const #{ var } = #{ parse value }"
    @vars[var] = true
    output
    
  when :gvasgn
    name, value = ast.children
    "#{ name } = #{ parse value }"
    
  when :ivasgn
    name, expression = ast.children
    "#{ name.to_s.sub('@', 'this._') } = #{ parse expression }"
    
  when :or_asgn
    var, value = ast.children
    "#{ parse var } = #{parse var} || #{ parse value }"

  when :and_asgn
    var, value = ast.children
    "#{ parse var } = #{parse var} && #{ parse value }"
    
  when :ivar
    name = ast.children.first
    name.to_s.sub('@', 'this._')
    
  when :hash
    hashy  = ast.children.map do |node|
      left, right = node.children
      key = parse left
      key = $1 if key =~ /\A"([a-zA-Z_$][a-zA-Z_$0-9]*)"\Z/
      "#{key}: #{parse right}"
    end
    "{#{ hashy.join(', ') }}"

  when :regexp
    str, opt = ast.children
    if str.children.first.include? '/'
      if opt.children.empty?
        "new RegExp(#{ str.children.first.inspect })"
      else
        "new RegExp(#{ str.children.first.inspect }, #{ opt.children.join.inspect})"
      end
    else
      "/#{ str.children.first }/#{ opt.children.join }"
    end

  when :array
    splat = ast.children.rindex { |a| a.type == :splat }
    if splat
      items = ast.children
      item = items[splat].children.first
      if items.length == 1
        parse item
      elsif splat == items.length - 1
        parse s(:send, s(:array, *items[0..-2]), :concat, item)
      elsif splat == 0
        parse s(:send, item, :concat, s(:array, *items[1..-1]))
      else
        parse s(:send, 
          s(:send, s(:array, *items[0..splat-1]), :concat, item), 
          :concat, s(:array, *items[splat+1..-1]))
      end
    else
      list = ast.children.map { |a| parse a }
      if list.join(', ').length < 80
        "[#{ list.join(', ') }]"
      else
        "[\n#{ list.join(",\n") }\n]"
      end
    end

  when :begin
    ast.children.map{ |e| parse e, :statement }.join(@sep)
    
  when :return
    "return #{ parse ast.children.first }"
    
  when *LOGICAL
    left, right = ast.children
    left = left.children.first if left and left.type == :begin
    right = right.children.first if right.type == :begin
    op_index    = operator_index ast.type
    lgroup      = LOGICAL.include?( left.type ) && op_index <= operator_index( left.type )
    left        = parse left
    left        = "(#{ left })" if lgroup
    rgroup      = LOGICAL.include?( right.type ) && op_index <= operator_index( right.type ) if right.children.length > 0
    right       = parse right
    right       = "(#{ right })" if rgroup

    case ast.type
    when :and
      "#{ left } && #{ right }"
    when :or
      "#{ left } || #{ right }"
    else
      "!#{ left }"
    end

  when :send, :attr
    receiver, method, *args = ast.children
    if method =~ /\w[!?]$/
      raise NotImplementedError, "invalid method name #{ method }"
    end

    if method == :new and receiver and receiver.children == [nil, :Proc]
      return parse args.first
    elsif not receiver and [:lambda, :proc].include? method
      return parse args.first
    end

    op_index   = operator_index method
    if op_index != -1
      target = args.first 
      target = target.children.first if target and target.type == :begin
      receiver = receiver.children.first if receiver.type == :begin
    end

    group_receiver = receiver.type == :send && op_index <= operator_index( receiver.children[1] ) if receiver
    group_target = target.type == :send && op_index <= operator_index( target.children[1] ) if target

    if method == :!
      if receiver.type == :defined?
        parse s(:undefined?, *receiver.children)
      else
        group_receiver ||= (receiver.type != :send && receiver.children.length > 1)
        "!#{ group_receiver ? group(receiver) : parse(receiver) }"
      end

    elsif method == :[]
      "#{ parse receiver }[#{ args.map {|arg| parse arg}.join(', ') }]"

    elsif method == :-@ or method == :+@
      "#{ method.to_s[0] }#{ parse receiver }"

    elsif method == :=~
      "#{ parse args.first }.test(#{ parse receiver })"

    elsif method == :!~
      "!#{ parse args.first }.test(#{ parse receiver })"

    elsif OPERATORS.flatten.include? method
      "#{ group_receiver ? group(receiver) : parse(receiver) } #{ method } #{ group_target ? group(target) : parse(target) }"  

    elsif method =~ /=$/
      "#{ parse receiver }#{ '.' if receiver }#{ method.to_s.sub(/=$/, ' =') } #{ parse args.first }"

    elsif method == :new and receiver
      args = args.map {|a| parse a}.join(', ')
      "new #{ parse receiver }(#{ args })"

    elsif method == :raise and receiver == nil
      if args.length == 1
        "throw #{ parse args.first }"
      else
        "throw new #{ parse args.first }(#{ parse args[1] })"
      end

    elsif method == :typeof and receiver == nil
      "typeof #{ parse args.first }"

    else
      if args.length == 0 and not is_method?(ast)
        "#{ parse receiver }#{ '.' if receiver }#{ method }"
      elsif args.length > 0 and args.last.type == :splat
        parse s(:send, s(:attr, receiver, method), :apply, receiver, 
          s(:send, s(:array, *args[0..-2]), :concat,
            args[-1].children.first))
      else
        args = args.map {|a| parse a}.join(', ')
        "#{ parse receiver }#{ '.' if receiver }#{ method }(#{ args })"
      end
    end
    
  when :const
    receiver, name = ast.children
    "#{ parse receiver }#{ '.' if receiver }#{ name }"

  when :masgn
    lhs, rhs = ast.children
    block = []
    lhs.children.zip rhs.children.zip do |var, val| 
      block << s(var.type, *var.children, *val)
    end
    parse s(:begin, *block)
  
  when :if
    condition, true_block, else_block = ast.children
    if state == :statement
      output = "if (#{ parse condition }) {#@nl#{ scope true_block }#@nl}"
      while else_block and else_block.type == :if
        condition, true_block, else_block = else_block.children
        output <<  " else if (#{ parse condition }) {#@nl#{ scope true_block }#@nl}"
      end
      output << " else {#@nl#{ scope else_block }#@nl}" if else_block
      output
    else
      "(#{ parse condition } ? #{ parse true_block } : #{ parse else_block })"
    end
    
  when :while
    condition, block = ast.children
    "while (#{ parse condition }) {#@nl#{ scope block }#@nl}"

  when :until
    condition, block = ast.children
    parse s(:while, s(:send, condition, :!), block)

  when :while_post
    condition, block = ast.children
    block = block.updated(:begin) if block.type == :kwbegin
    "do {#@nl#{ scope block }#@nl} while (#{ parse condition })"

  when :until_post
    condition, block = ast.children
    parse s(:while_post, s(:send, condition, :!), block)

  when :for
    var, expression, block = ast.children
    parse s(:block, 
      s(:send, expression, :forEach),
      s(:args, s(:arg, var.children.last)),
      block);

  when :case
    expr, *whens, other = ast.children

    whens.map! do |node|
      *values, code = node.children
      cases = values.map {|value| "case #{ parse value }:#@ws"}.join
      "#{ cases }#{ parse code }#{@sep}break#@sep"
    end

    other = "#{@nl}default:#@ws#{ parse other }#@nl" if other

    "switch (#{ parse expr }) {#@nl#{whens.join(@nl)}#{other}}"

  when :block
    call, args, block = ast.children
    block ||= s(:begin)
    function = s(:def, name, args, block)
    parse s(:send, *call.children, function)
  
  when :def
    name, args, body = ast.children
    body ||= s(:begin)
    if name =~ /[!?]$/
      raise NotImplementedError, "invalid method name #{ name }"
    end

    vars = {}
    if args and !args.children.empty?
      # splats
      if args.children.last.type == :restarg
        if args.children[-1].children.first
          body = s(:begin, body) unless body.type == :begin
          assign = s(:lvasgn, args.children[-1].children.first,
            s(:send, s(:attr, 
              s(:attr, s(:const, nil, :Array), :prototype), :slice),
              :call, s(:lvar, :arguments),
              s(:int, args.children.length-1)))
          body = s(:begin, assign, *body.children)
        end

        args = s(:args, *args.children[0..-2])

      elsif args.children.last.type == :blockarg and
        args.children.length > 1 and args.children[-2].type == :restarg
        body = s(:begin, body) unless body.type == :begin
        blk = args.children[-1].children.first
        vararg = args.children[-2].children.first
        last = s(:send, s(:attr, s(:lvar, :arguments), :length), :-,
                s(:int, 1))

        # set block argument to the last argument passed
        assign2 = s(:lvasgn, blk, s(:send, s(:lvar, :arguments), :[], last))

        if vararg
          # extract arguments between those defined and the last
          assign1 = s(:lvasgn, vararg, s(:send, s(:attr, s(:attr, s(:const,
            nil, :Array), :prototype), :slice), :call, s(:lvar, :arguments),
            s(:int, args.children.length-1), last))
          # push block argument back onto args if not a function
          pushback = s(:if, s(:send, s(:send, nil, :typeof, s(:lvar, blk)), 
            :"!==", s(:str, "function")), s(:begin, s(:send, s(:lvar,
            vararg), :push, s(:lvar, blk)), s(:lvasgn, blk, s(:nil))), nil)
          # set block argument to null if all arguments were defined
          pushback = s(:if, s(:send, s(:attr, s(:lvar, :arguments),
            :length), :<=, s(:int, args.children.length-2)), s(:lvasgn, 
            blk, s(:nil)), pushback)
          # combine statements
          body = s(:begin, assign1, assign2, pushback, *body.children)
        else
          # set block argument to null if all arguments were defined
          ignore = s(:if, s(:send, s(:attr, s(:lvar, :arguments),
            :length), :<=, s(:int, args.children.length-2)), s(:lvasgn, 
            blk, s(:nil)), nil)
          body = s(:begin, assign2, ignore, *body.children)
        end

        args = s(:args, *args.children[0..-3])
      end

      # optional arguments
      args.children.each_with_index do |arg, i|
        if arg.type == :optarg
          body = s(:begin, body) unless body.type == :begin
          argname, value = arg.children
          children = args.children.dup
          children[i] = s(:arg, argname)
          args = s(:args, *children)
          body = s(:begin, body) unless body.type == :begin
          default = s(:if, s(:send, s(:defined?, s(:lvar, argname)), :!),
            s(:lvasgn, argname, value), nil)
          body = s(:begin, default, *body.children)
        end
        vars[arg.children.first] = true
      end
    end

    "function#{ " #{name}" if name }(#{ parse args }) {#@nl#{ scope body, vars}#{@nl unless body == s(:begin)}}"

  when :class
    name, inheritance, *body = ast.children
    init = s(:def, :initialize, s(:args))
    body.compact!

    if body.length == 1 and body.first.type == :begin
      body = body.first.children.dup 
    end

    body.map! do |m| 
      if m.type == :def
        if m.children.first == :initialize
          # constructor: remove from body and overwrite init function
          init = m
          nil
        else
          # method: add to prototype
          s(:send, s(:attr, name, :prototype), "#{m.children[0]}=",
            s(:block, s(:send, nil, :proc), *m.children[1..-1]))
        end
      elsif m.type == :defs and m.children.first == s(:self)
        # class method definition: add to prototype
        s(:send, name, "#{m.children[1]}=",
          s(:block, s(:send, nil, :proc), *m.children[2..-1]))
      elsif m.type == :send and m.children.first == nil
        # class method call
        s(:send, name, *m.children[1..-1])
      elsif m.type == :lvasgn
        # class variable
        s(:send, name, "#{m.children[0]}=", *m.children[1..-1])
      elsif m.type == :casgn and m.children[0] == nil
        # class constant
        s(:send, name, "#{m.children[1]}=", *m.children[2..-1])
      else
        raise NotImplementedError, "class #{ m.type }"
      end
    end

    if inheritance
      body.unshift s(:send, name, :prototype=, s(:send, inheritance, :new))
    end

    # prepend constructor
    body.unshift s(:def, parse(name), *init.children[1..-1])

    parse s(:begin, *body.compact)

  when :args
    ast.children.map { |a| parse a }.join(', ')

  when :arg, :blockarg
    ast.children.first

  when :block_pass
    parse ast.children.first
    
  when :dstr, :dsym
    ast.children.map{ |s| parse s }.join(' + ')
    
  when :xstr
    if @binding
      @binding.eval(ast.children.first.children.first).to_s
    else
      eval(ast.children.first.children.first).to_s
    end

  when :self
    'this'

  when :break
    'break'

  when :next
    'continue'

  when :defined?
    "typeof #{ parse ast.children.first } !== 'undefined'"

  when :undefined?
    "typeof #{ parse ast.children.first } === 'undefined'"

  when :undef
    ast.children.map {|c| "delete #{c.children.last}"}.join @sep

  when :kwbegin
    block = ast.children.first
    if block.type == :ensure
      block, finally = block.children
    else
      finally = nil
    end

    if block and block.type == :rescue
      body, recover, otherwise = block.children
      raise NotImplementedError, "block else" if otherwise
      exception, name, recovery = recover.children
      raise NotImplementedError, parse(exception) if exception
    else
      body = block
    end

    output = "try {#@nl#{ parse body }#@nl}"
    output += " catch (#{ parse name }) {#@nl#{ parse recovery }#@nl}" if recovery
    output += " finally {#@nl#{ parse finally }#@nl}" if finally

    if recovery or finally
      output
    else
      parse s(:begin, *ast.children)
    end

  else 
    raise NotImplementedError, "unknown AST type #{ ast.type }"
  end
end

#s(type, *args) ⇒ Object



45
46
47
# File 'lib/ruby2js/converter.rb', line 45

def s(type, *args)
  Parser::AST::Node.new(type, args)
end

#scope(ast, args = {}) ⇒ Object



37
38
39
40
41
42
43
# File 'lib/ruby2js/converter.rb', line 37

def scope( ast, args={} )
  @varstack.push @vars
  @vars = @vars.merge(args)
  parse( ast, :statement )
ensure
  @vars = @varstack.pop
end

#to_jsObject



29
30
31
# File 'lib/ruby2js/converter.rb', line 29

def to_js
  parse( @ast, :statement )
end