ruby2js 1.15.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. data/lib/ruby2js.rb +36 -36
  2. data/lib/ruby2js/converter.rb +59 -20
  3. data/lib/ruby2js/converter/arg.rb +1 -1
  4. data/lib/ruby2js/converter/args.rb +1 -1
  5. data/lib/ruby2js/converter/array.rb +3 -4
  6. data/lib/ruby2js/converter/begin.rb +15 -1
  7. data/lib/ruby2js/converter/block.rb +6 -5
  8. data/lib/ruby2js/converter/boolean.rb +1 -1
  9. data/lib/ruby2js/converter/break.rb +1 -1
  10. data/lib/ruby2js/converter/case.rb +27 -7
  11. data/lib/ruby2js/converter/casgn.rb +5 -2
  12. data/lib/ruby2js/converter/class.rb +41 -11
  13. data/lib/ruby2js/converter/const.rb +1 -1
  14. data/lib/ruby2js/converter/cvar.rb +4 -3
  15. data/lib/ruby2js/converter/cvasgn.rb +5 -6
  16. data/lib/ruby2js/converter/def.rb +15 -3
  17. data/lib/ruby2js/converter/defined.rb +1 -1
  18. data/lib/ruby2js/converter/defs.rb +7 -3
  19. data/lib/ruby2js/converter/dstr.rb +3 -3
  20. data/lib/ruby2js/converter/for.rb +7 -10
  21. data/lib/ruby2js/converter/hash.rb +70 -34
  22. data/lib/ruby2js/converter/if.rb +35 -13
  23. data/lib/ruby2js/converter/in.rb +1 -1
  24. data/lib/ruby2js/converter/ivasgn.rb +1 -1
  25. data/lib/ruby2js/converter/kwbegin.rb +20 -20
  26. data/lib/ruby2js/converter/literal.rb +1 -1
  27. data/lib/ruby2js/converter/logical.rb +4 -8
  28. data/lib/ruby2js/converter/next.rb +1 -1
  29. data/lib/ruby2js/converter/nil.rb +1 -1
  30. data/lib/ruby2js/converter/nthref.rb +1 -1
  31. data/lib/ruby2js/converter/opasgn.rb +3 -3
  32. data/lib/ruby2js/converter/regexp.rb +12 -9
  33. data/lib/ruby2js/converter/return.rb +3 -3
  34. data/lib/ruby2js/converter/self.rb +2 -2
  35. data/lib/ruby2js/converter/send.rb +31 -30
  36. data/lib/ruby2js/converter/super.rb +8 -11
  37. data/lib/ruby2js/converter/sym.rb +1 -1
  38. data/lib/ruby2js/converter/undef.rb +9 -2
  39. data/lib/ruby2js/converter/var.rb +1 -1
  40. data/lib/ruby2js/converter/vasgn.rb +13 -5
  41. data/lib/ruby2js/converter/while.rb +2 -1
  42. data/lib/ruby2js/converter/whilepost.rb +2 -1
  43. data/lib/ruby2js/converter/xstr.rb +4 -3
  44. data/lib/ruby2js/execjs.rb +3 -3
  45. data/lib/ruby2js/filter/camelCase.rb +8 -8
  46. data/lib/ruby2js/filter/functions.rb +64 -65
  47. data/lib/ruby2js/filter/react.rb +44 -16
  48. data/lib/ruby2js/filter/require.rb +4 -1
  49. data/lib/ruby2js/filter/underscore.rb +21 -21
  50. data/lib/ruby2js/serializer.rb +347 -0
  51. data/lib/ruby2js/version.rb +3 -3
  52. data/ruby2js.gemspec +3 -3
  53. metadata +3 -2
data/lib/ruby2js.rb CHANGED
@@ -13,18 +13,40 @@ module Ruby2JS
13
13
  def s(type, *args)
14
14
  Parser::AST::Node.new type, args
15
15
  end
16
+
17
+ def S(type, *args)
18
+ @ast.updated(type, args)
19
+ end
16
20
  end
17
21
 
18
22
  class Processor < Parser::AST::Processor
19
23
  BINARY_OPERATORS = Converter::OPERATORS[2..-1].flatten
20
24
 
25
+ def initialize(comments)
26
+ @comments = comments
27
+ end
28
+
21
29
  def options=(options)
22
30
  @options = options
23
31
  end
24
32
 
33
+ def process(node)
34
+ ast, @ast = @ast, node
35
+ replacement = super
36
+
37
+ if replacement != node and @comments[node]
38
+ @comments[replacement] = @comments[node]
39
+ end
40
+
41
+ replacement
42
+ ensure
43
+ @ast = ast
44
+ end
45
+
25
46
  # handle all of the 'invented' ast types
26
47
  def on_attr(node); on_send(node); end
27
48
  def on_autoreturn(node); on_return(node); end
49
+ def on_call(node); on_send(node); end
28
50
  def on_constructor(node); on_def(node); end
29
51
  def on_defp(node); on_defs(node); end
30
52
  def on_method(node); on_send(node); end
@@ -56,17 +78,19 @@ module Ruby2JS
56
78
  end
57
79
 
58
80
  def self.convert(source, options={})
59
-
60
81
  if Proc === source
61
82
  file,line = source.source_location
62
83
  source = File.read(file.dup.untaint).untaint
63
- ast = find_block( parse(source), line )
64
- options[:file] = file
84
+ ast, comments = parse(source)
85
+ comments = Parser::Source::Comment.associate(ast, comments) if ast
86
+ ast = find_block( ast, line )
87
+ options = options.merge(file => file) unless options[:file]
65
88
  elsif Parser::AST::Node === source
66
- ast = source
89
+ ast, comments = source, {}
67
90
  source = ast.loc.expression.source_buffer.source
68
91
  else
69
- ast = parse( source, options[:file] )
92
+ ast, comments = parse( source, options[:file] )
93
+ comments = Parser::Source::Comment.associate(ast, comments) if ast
70
94
  end
71
95
 
72
96
  filters = options[:filters] || Filter::DEFAULTS
@@ -76,12 +100,12 @@ module Ruby2JS
76
100
  filters.reverse.each do |mod|
77
101
  filter = Class.new(filter) {include mod}
78
102
  end
79
- filter = filter.new
103
+ filter = filter.new(comments)
80
104
  filter.options = options
81
105
  ast = filter.process(ast)
82
106
  end
83
107
 
84
- ruby2js = Ruby2JS::Converter.new( ast )
108
+ ruby2js = Ruby2JS::Converter.new(ast, comments)
85
109
 
86
110
  ruby2js.binding = options[:binding]
87
111
  ruby2js.ivars = options[:ivars]
@@ -96,44 +120,20 @@ module Ruby2JS
96
120
 
97
121
  ruby2js.width = options[:width] if options[:width]
98
122
 
99
- if source.include? "\n"
100
- ruby2js.enable_vertical_whitespace
101
- lines = ruby2js.to_js.split("\n")
102
- pre = []
103
- pending = false
104
- blank = true
105
- lines.each do |line|
106
- next if line.empty?
107
-
108
- if ')}]'.include? line[0]
109
- pre.pop
110
- line.sub!(/([,;])$/,"\\1\n")
111
- pending = true
112
- else
113
- pending = false
114
- end
123
+ ruby2js.enable_vertical_whitespace if source.include? "\n"
115
124
 
116
- line.insert 0, pre.join
117
- if '({['.include? line[-1]
118
- pre.push ' '
119
- line.insert 0, "\n" unless blank or pending
120
- pending = true
121
- end
125
+ ruby2js.convert
122
126
 
123
- blank = pending
124
- end
127
+ ruby2js.timestamp options[:file]
125
128
 
126
- lines.join("\n").gsub(/^ ( *(case.*|default):$)/, '\1')
127
- else
128
- ruby2js.to_js
129
- end
129
+ ruby2js
130
130
  end
131
131
 
132
132
  def self.parse(source, file=nil)
133
133
  # workaround for https://github.com/whitequark/parser/issues/112
134
134
  buffer = Parser::Source::Buffer.new(file || '__SOURCE__')
135
135
  buffer.raw_source = source.encode('utf-8')
136
- Parser::CurrentRuby.new.parse(buffer)
136
+ Parser::CurrentRuby.new.parse_with_comments(buffer)
137
137
  rescue Parser::SyntaxError => e
138
138
  split = source[0..e.diagnostic.location.begin_pos].split("\n")
139
139
  line, col = split.length, split.last.length
@@ -1,7 +1,11 @@
1
1
  require 'parser/current'
2
2
 
3
+ require 'ruby2js/serializer'
4
+
3
5
  module Ruby2JS
4
- class Converter
6
+ class Converter < Serializer
7
+ attr_accessor :ast
8
+
5
9
  LOGICAL = :and, :not, :or
6
10
  OPERATORS = [:[], :[]=], [:not, :!], [:*, :/, :%], [:+, :-], [:>>, :<<],
7
11
  [:&], [:^, :|], [:<=, :<, :>, :>=], [:==, :!=, :===, :"!=="], [:and, :or]
@@ -20,14 +24,13 @@ module Ruby2JS
20
24
 
21
25
  attr_accessor :binding, :ivars
22
26
 
23
- def initialize( ast, vars = {} )
24
- @ast, @vars = ast, vars.dup
25
- @sep = '; '
26
- @nl = ''
27
- @ws = ' '
27
+ def initialize( ast, comments, vars = {} )
28
+ super()
29
+
30
+ @ast, @comments, @vars = ast, comments, vars.dup
28
31
  @varstack = []
32
+ @scope = true
29
33
  @rbstack = []
30
- @width = 80
31
34
  @next_token = :return
32
35
 
33
36
  @handlers = {}
@@ -35,12 +38,6 @@ module Ruby2JS
35
38
  @handlers[name] = method("on_#{name}")
36
39
  end
37
40
  end
38
-
39
- def enable_vertical_whitespace
40
- @sep = ";\n"
41
- @nl = "\n"
42
- @ws = @nl
43
- end
44
41
 
45
42
  def binding=(binding)
46
43
  @binding = binding
@@ -50,7 +47,7 @@ module Ruby2JS
50
47
  @width = width
51
48
  end
52
49
 
53
- def to_js
50
+ def convert
54
51
  parse( @ast, :statement )
55
52
  end
56
53
 
@@ -59,11 +56,14 @@ module Ruby2JS
59
56
  end
60
57
 
61
58
  def scope( ast, args=nil )
62
- @varstack.push @vars.dup
59
+ scope, @scope = @scope, true
60
+ @varstack.push @vars
63
61
  @vars = args if args
62
+ @vars = Hash[@vars.map {|key, value| [key, true]}]
64
63
  parse( ast, :statement )
65
64
  ensure
66
65
  @vars = @varstack.pop
66
+ @scope = scope
67
67
  end
68
68
 
69
69
  def s(type, *args)
@@ -78,22 +78,60 @@ module Ruby2JS
78
78
  end
79
79
  end
80
80
 
81
+ # extract comments that either precede or are included in the node.
82
+ # remove from the list this node may appear later in the tree.
83
+ def comments(ast)
84
+ if ast.loc and ast.loc.respond_to? :expression
85
+ expression = ast.loc.expression
86
+
87
+ list = @comments[ast].select do |comment|
88
+ expression.source_buffer == comment.loc.expression.source_buffer and
89
+ comment.loc.expression.begin_pos < expression.end_pos
90
+ end
91
+ else
92
+ list = @comments[ast]
93
+ end
94
+
95
+ @comments[ast] -= list
96
+
97
+ list.map do |comment|
98
+ comment.text.sub(/^#/, '//') + "\n"
99
+ end
100
+ end
101
+
81
102
  def parse(ast, state=:expression)
82
- return ast unless ast
103
+ oldstate, @state = @state, state
104
+ oldast, @ast = @ast, ast
105
+ return unless ast
83
106
 
84
- @state = state
85
- @ast = ast
86
107
  handler = @handlers[ast.type]
87
108
 
88
109
  unless handler
89
110
  raise NotImplementedError, "unknown AST type #{ ast.type }"
90
111
  end
91
112
 
92
- handler.call(*ast.children) if handler
113
+ if state == :statement and not @comments[ast].empty?
114
+ comments(ast).each {|comment| puts comment.chomp}
115
+ end
116
+
117
+ handler.call(*ast.children)
118
+ ensure
119
+ @ast = oldast
120
+ end
121
+
122
+ def parse_all(*args)
123
+ options = (Hash === args.last) ? args.pop : {}
124
+ sep = options[:join].to_s
125
+ state = options[:state] || :expression
126
+
127
+ args.each_with_index do |arg, index|
128
+ put sep unless index == 0
129
+ parse arg, state
130
+ end
93
131
  end
94
132
 
95
133
  def group( ast )
96
- "(#{ parse ast })"
134
+ put '('; parse ast; put ')'
97
135
  end
98
136
  end
99
137
  end
@@ -103,6 +141,7 @@ module Parser
103
141
  class Node
104
142
  def is_method?
105
143
  return false if type == :attr
144
+ return true if type == :call
106
145
  return true unless loc
107
146
 
108
147
  if loc.respond_to? :selector
@@ -8,7 +8,7 @@ module Ruby2JS
8
8
 
9
9
  handle :arg, :blockarg do |arg, unknown=nil|
10
10
  raise NotImplementedError, "argument #{ unknown.inspect }" if unknown
11
- arg
11
+ put arg
12
12
  end
13
13
 
14
14
  # (shadowarg :a)
@@ -7,7 +7,7 @@ module Ruby2JS
7
7
  # (blockarg :c))
8
8
 
9
9
  handle :args do |*args|
10
- args.map { |arg| parse arg }.compact.join(', ')
10
+ parse_all *args, join: ', '
11
11
  end
12
12
  end
13
13
  end
@@ -21,11 +21,10 @@ module Ruby2JS
21
21
  :concat, s(:array, *items[splat+1..-1]))
22
22
  end
23
23
  else
24
- items.map! { |item| parse item }
25
- if items.map {|item| item.length+2}.reduce(&:+).to_i < @width-8
26
- "[#{ items.join(', ') }]"
24
+ if items.length <= 1
25
+ put '['; parse_all *items, join: ', '; put ']'
27
26
  else
28
- "[#@nl#{ items.join(",#@ws") }#@nl]"
27
+ compact { puts '['; parse_all *items, join: ",#{@ws}"; sput ']' }
29
28
  end
30
29
  end
31
30
  end
@@ -28,7 +28,7 @@ module Ruby2JS
28
28
  statements.compact!
29
29
  end
30
30
 
31
- statements.map{ |statement| parse statement, state }.join(@sep)
31
+ parse_all *statements, state: state, join: @sep
32
32
  end
33
33
 
34
34
  def combine_properties(body)
@@ -36,7 +36,21 @@ module Ruby2JS
36
36
  next unless body[i] and body[i].type == :prop
37
37
  for j in i+1...body.length
38
38
  break unless body[j] and body[j].type == :prop
39
+
39
40
  if body[i].children[0] == body[j].children[0]
41
+ # relocate property comment to first method
42
+ [body[i], body[j]].each do |node|
43
+ unless @comments[node].empty?
44
+ node.children[1].values.first.each do |key, value|
45
+ if [:get, :set].include? key and Parser::AST::Node === value
46
+ @comments[value] = @comments[node]
47
+ break
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ # merge properties
40
54
  merge = Hash[(body[i].children[1].to_a+body[j].children[1].to_a).
41
55
  group_by {|name, value| name.to_s}.map {|name, values|
42
56
  [name, values.map(&:last).reduce(:merge)]}]
@@ -17,13 +17,14 @@ module Ruby2JS
17
17
  var = args.children.first
18
18
  expression = call.children.first.children.first
19
19
  comp = (expression.type == :irange ? '<=' : '<')
20
- "for (var #{parse var} = #{ parse expression.children.first }; " +
21
- "#{ parse var } #{ comp } #{ parse expression.children.last }; " +
22
- "#{ parse var } += #{ parse call.children[2] }) " +
23
- "{#@nl#{ scope block }#@nl}"
20
+ put "for (var "; parse var; put " = "; parse expression.children.first
21
+ put "; "; parse var; put " #{comp} "; parse expression.children.last
22
+ put "; "; parse var; put " += "; parse call.children[2]; puts ") {"
23
+ scope block
24
+ sput "}"
24
25
  else
25
26
  block ||= s(:begin)
26
- function = s(:def, nil, args, block)
27
+ function = @ast.updated(:def, [nil, args, block])
27
28
  parse s(:send, *call.children, function)
28
29
  end
29
30
  end
@@ -5,7 +5,7 @@ module Ruby2JS
5
5
  # (false)
6
6
 
7
7
  handle :true, :false do
8
- @ast.type.to_s
8
+ put @ast.type.to_s
9
9
  end
10
10
  end
11
11
  end
@@ -7,7 +7,7 @@ module Ruby2JS
7
7
  handle :break do |n=nil|
8
8
  raise NotImplementedError, "break argument #{ n.inspect }" if n
9
9
  raise NotImplementedError, "break outside of loop" if @next_token == :return
10
- 'break'
10
+ put 'break'
11
11
  end
12
12
  end
13
13
  end
@@ -9,15 +9,35 @@ module Ruby2JS
9
9
  # (...))
10
10
 
11
11
  handle :case do |expr, *whens, other|
12
- whens.map! do |node|
13
- *values, code = node.children
14
- cases = values.map {|value| "case #{ parse value }:#@ws"}.join
15
- "#{ cases }#{ parse code, :statement }#{@sep}break#@sep"
16
- end
12
+ begin
13
+ scope, @scope = @scope, false
14
+ mark = output_location
15
+
16
+ put 'switch ('; parse expr; puts ') {'
17
+
18
+ whens.each_with_index do |node, index|
19
+ puts '' unless index == 0
17
20
 
18
- other = "#{@nl}default:#@ws#{ parse other, :statement }#@nl" if other
21
+ *values, code = node.children
22
+ values.each {|value| put 'case '; parse value; put ":#@ws"}
23
+ parse code, :statement
24
+ put "#{@sep}break#@sep" if other or index < whens.length-1
25
+ end
19
26
 
20
- "switch (#{ parse expr }) {#@nl#{whens.join(@nl)}#{other}}"
27
+ (put "#{@nl}default:#@ws"; parse other, :statement) if other
28
+
29
+ sput '}'
30
+
31
+ if scope
32
+ vars = @vars.select {|key, value| value == :pending}.keys
33
+ unless vars.empty?
34
+ insert mark, "var #{vars.join(', ')}#{@sep}"
35
+ vars.each {|var| @vars[var] = true}
36
+ end
37
+ end
38
+ ensure
39
+ @scope = scope
40
+ end
21
41
  end
22
42
  end
23
43
  end
@@ -6,8 +6,11 @@ module Ruby2JS
6
6
 
7
7
  handle :casgn do |cbase, var, value|
8
8
  begin
9
- var = "#{ parse cbase }.var" if cbase
10
- "var #{ var } = #{ parse value }"
9
+ put "var "
10
+
11
+ (parse cbase; put '.') if cbase
12
+
13
+ put "#{ var } = "; parse value
11
14
  ensure
12
15
  @vars[var] = true
13
16
  end
@@ -12,7 +12,7 @@ module Ruby2JS
12
12
  if inheritance
13
13
  init = s(:def, :initialize, s(:args), s(:super))
14
14
  else
15
- init = s(:def, :initialize, s(:args))
15
+ init = s(:def, :initialize, s(:args), nil)
16
16
  end
17
17
 
18
18
  body.compact!
@@ -24,7 +24,7 @@ module Ruby2JS
24
24
  body.compact!
25
25
  visible = {}
26
26
  body.map! do |m|
27
- if m.type == :def
27
+ node = if m.type == :def
28
28
  if m.children.first == :initialize
29
29
  # constructor: remove from body and overwrite init function
30
30
  init = m
@@ -43,7 +43,7 @@ module Ruby2JS
43
43
  s(:prop, s(:attr, name, :prototype), m.children.first =>
44
44
  {enumerable: s(:true), configurable: s(:true),
45
45
  get: s(:block, s(:send, nil, :proc), m.children[1],
46
- s(:autoreturn, *m.children[2..-1]))})
46
+ m.updated(:autoreturn, m.children[2..-1]))})
47
47
  else
48
48
  # method: add to prototype
49
49
  s(:method, s(:attr, name, :prototype),
@@ -66,7 +66,7 @@ module Ruby2JS
66
66
  s(:prop, name, m.children[1].to_s =>
67
67
  {enumerable: s(:true), configurable: s(:true),
68
68
  get: s(:block, s(:send, nil, :proc), m.children[2],
69
- s(:autoreturn, *m.children[3..-1]))})
69
+ m.updated(:autoreturn, m.children[3..-1]))})
70
70
  else
71
71
  # class method definition: add to prototype
72
72
  s(:prototype, s(:send, name, "#{m.children[1]}=",
@@ -134,6 +134,19 @@ module Ruby2JS
134
134
  else
135
135
  raise NotImplementedError, "class #{ m.type }"
136
136
  end
137
+
138
+ # associate comments
139
+ if node and @comments[m]
140
+ if Array === node
141
+ node[0] = m.updated(node.first.type, node.first.children)
142
+ @comments[node.first] = @comments[m]
143
+ else
144
+ node = m.updated(node.type, node.children)
145
+ @comments[node] = @comments[m]
146
+ end
147
+ end
148
+
149
+ node
137
150
  end
138
151
 
139
152
  body.flatten!
@@ -167,12 +180,23 @@ module Ruby2JS
167
180
  if methods > 1 or (methods == 1 and body[start].type == :prop)
168
181
  pairs = body[start...start+methods].map do |node|
169
182
  if node.type == :method
170
- s(:pair, s(:str, node.children[1].to_s.chomp('=')),
171
- node.children[2])
183
+ replacement = node.updated(:pair, [
184
+ s(:str, node.children[1].to_s.chomp('=')),
185
+ node.children[2]])
172
186
  else
173
- node.children[1].map {|prop, descriptor|
174
- s(:pair, s(:prop, prop), descriptor)}
187
+ replacement = node.children[1].map do |prop, descriptor|
188
+ node.updated(:pair, [s(:prop, prop), descriptor])
189
+ end
190
+ end
191
+
192
+ if @comments[node]
193
+ if Array === replacement
194
+ @comments[replacement.first] = @comments[node]
195
+ else
196
+ @comments[replacement] = @comments[node]
197
+ end
175
198
  end
199
+ replacement
176
200
  end
177
201
  body[start...start+methods] =
178
202
  s(:send, name, :prototype=, s(:hash, *pairs.flatten))
@@ -180,7 +204,9 @@ module Ruby2JS
180
204
  end
181
205
 
182
206
  # prepend constructor
183
- body.unshift s(:constructor, parse(name), *init.children[1..-1])
207
+ constructor = init.updated(:constructor, [name, *init.children[1..-1]])
208
+ @comments[constructor] = @comments[init] unless @comments[init].empty?
209
+ body.unshift constructor
184
210
 
185
211
  begin
186
212
  # save class name
@@ -193,7 +219,7 @@ module Ruby2JS
193
219
  # add locally visible interfaces to rbstack. See send.rb, const.rb
194
220
  @rbstack.push visible
195
221
 
196
- parse s(:begin, *body.compact)
222
+ parse s(:begin, *body.compact), :statement
197
223
  ensure
198
224
  self.ivars = ivars
199
225
  @class_name = class_name
@@ -224,8 +250,12 @@ module Ruby2JS
224
250
  end
225
251
  elsif @ast.type == :method
226
252
  parse s(:send, *args)
253
+ elsif args.first.children.first
254
+ parse s(:send, args.first.children.first,
255
+ "#{args.first.children[1]}=", s(:block, s(:send, nil, :proc),
256
+ *args[1..-1]))
227
257
  else
228
- parse s(:def, *args)
258
+ parse s(:def, args.first.children[1], *args[1..-1])
229
259
  end
230
260
  ensure
231
261
  @instance_method = instance_method