ruby2js 1.15.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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