ruby2js 3.6.1 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -7
  3. data/lib/ruby2js.rb +32 -9
  4. data/lib/ruby2js/converter.rb +8 -2
  5. data/lib/ruby2js/converter/assign.rb +159 -0
  6. data/lib/ruby2js/converter/begin.rb +7 -2
  7. data/lib/ruby2js/converter/case.rb +7 -2
  8. data/lib/ruby2js/converter/class.rb +77 -21
  9. data/lib/ruby2js/converter/class2.rb +39 -11
  10. data/lib/ruby2js/converter/def.rb +6 -2
  11. data/lib/ruby2js/converter/dstr.rb +8 -3
  12. data/lib/ruby2js/converter/hash.rb +9 -5
  13. data/lib/ruby2js/converter/hide.rb +13 -0
  14. data/lib/ruby2js/converter/if.rb +10 -2
  15. data/lib/ruby2js/converter/import.rb +18 -3
  16. data/lib/ruby2js/converter/kwbegin.rb +9 -2
  17. data/lib/ruby2js/converter/literal.rb +2 -2
  18. data/lib/ruby2js/converter/module.rb +37 -5
  19. data/lib/ruby2js/converter/opasgn.rb +8 -0
  20. data/lib/ruby2js/converter/send.rb +41 -2
  21. data/lib/ruby2js/converter/vasgn.rb +5 -0
  22. data/lib/ruby2js/demo.rb +53 -0
  23. data/lib/ruby2js/es2022.rb +5 -0
  24. data/lib/ruby2js/es2022/strict.rb +3 -0
  25. data/lib/ruby2js/filter.rb +9 -1
  26. data/lib/ruby2js/filter/active_functions.rb +1 -0
  27. data/lib/ruby2js/filter/cjs.rb +2 -0
  28. data/lib/ruby2js/filter/esm.rb +44 -10
  29. data/lib/ruby2js/filter/functions.rb +84 -95
  30. data/lib/ruby2js/filter/{wunderbar.rb → jsx.rb} +29 -7
  31. data/lib/ruby2js/filter/react.rb +191 -56
  32. data/lib/ruby2js/filter/require.rb +100 -5
  33. data/lib/ruby2js/filter/return.rb +13 -1
  34. data/lib/ruby2js/filter/stimulus.rb +185 -0
  35. data/lib/ruby2js/jsx.rb +291 -0
  36. data/lib/ruby2js/namespace.rb +75 -0
  37. data/lib/ruby2js/version.rb +3 -3
  38. metadata +12 -4
@@ -1,4 +1,5 @@
1
1
  require 'ruby2js'
2
+ require 'pathname'
2
3
 
3
4
  module Ruby2JS
4
5
  module Filter
@@ -13,9 +14,17 @@ module Ruby2JS
13
14
 
14
15
  def initialize(*args)
15
16
  @require_expr = nil
17
+ @require_seen = {}
18
+ @require_relative = '.'
16
19
  super
17
20
  end
18
21
 
22
+ def options=(options)
23
+ super
24
+ @require_autoexports = !@disable_autoexports && options[:autoexports]
25
+ @require_recursive = options[:require_recursive]
26
+ end
27
+
19
28
  def on_send(node)
20
29
  if \
21
30
  not @require_expr and # only statements
@@ -44,11 +53,97 @@ module Ruby2JS
44
53
  filename += '.js.rb'
45
54
  end
46
55
 
47
- @options[:file2] = filename
48
- ast, comments = Ruby2JS.parse(File.read(filename), filename)
49
- @comments.merge! Parser::Source::Comment.associate(ast, comments)
50
- @comments[node] += @comments[ast]
51
- process ast
56
+ realpath = File.realpath(filename)
57
+ if @require_seen[realpath]
58
+ ast = s(:hide)
59
+ else
60
+ @require_seen[realpath] = []
61
+
62
+ @options[:file2] = filename
63
+ ast, comments = Ruby2JS.parse(File.read(filename), filename)
64
+ @comments.merge! Parser::Source::Comment.associate(ast, comments)
65
+ @comments[node] += @comments[ast]
66
+ end
67
+
68
+ children = ast.type == :begin ? ast.children : [ast]
69
+
70
+ named_exports = []
71
+ auto_exports = []
72
+ default_exports = []
73
+ children.each do |child|
74
+ if child&.type == :send and child.children[0..1] == [nil, :export]
75
+ child = child.children[2]
76
+ if child&.type == :send and child.children[0..1] == [nil, :default]
77
+ child = child.children[2]
78
+ target = default_exports
79
+ else
80
+ target = named_exports
81
+ end
82
+ elsif @require_autoexports
83
+ target = auto_exports
84
+ else
85
+ next
86
+ end
87
+
88
+ if %i[class module].include? child.type and child.children[0].children[0] == nil
89
+ target << child.children[0].children[1]
90
+ elsif child.type == :casgn and child.children[0] == nil
91
+ target << child.children[1]
92
+ elsif child.type == :def
93
+ target << child.children[0]
94
+ end
95
+ end
96
+
97
+ if @require_autoexports == :default and auto_exports.length == 1
98
+ default_exports += auto_exports
99
+ else
100
+ named_exports += auto_exports
101
+ end
102
+
103
+ imports = @require_seen[realpath]
104
+ imports << s(:const, nil, default_exports.first) unless default_exports.empty?
105
+ imports << named_exports.map {|id| s(:const, nil, id)} unless named_exports.empty?
106
+
107
+ if imports.empty?
108
+ process ast
109
+ else
110
+ @require_seen[realpath] = imports
111
+
112
+ importname = Pathname.new(filename).relative_path_from(Pathname.new(dirname)).to_s
113
+ importname = Pathname.new(@require_relative).join(importname).to_s
114
+
115
+ prepend_list << s(:import, importname, *imports)
116
+
117
+ save_prepend_list = prepend_list.dup
118
+
119
+ begin
120
+ require_relative = @require_relative
121
+ @require_relative = Pathname.new(@require_relative).join(basename).parent.to_s
122
+ node = process s(:hide, ast)
123
+ ensure
124
+ @require_relative = require_relative
125
+ end
126
+
127
+ if @require_recursive
128
+ block = node.children
129
+ while block.length == 1 and block.first.type == :begin
130
+ block = block.first.children
131
+ end
132
+
133
+ block.each do |child|
134
+ if child&.type == :import
135
+ puts ['rr', basename, child.inspect]
136
+ prepend_list << child
137
+ end
138
+ end
139
+ else
140
+ prepend_list.keep_if do |import|
141
+ save_prepend_list.include? import
142
+ end
143
+ end
144
+
145
+ node
146
+ end
52
147
  ensure
53
148
  if file2
54
149
  @options[:file2] = file2
@@ -20,7 +20,7 @@ module Ruby2JS
20
20
 
21
21
  def on_def(node)
22
22
  node = super
23
- return node unless node.type == :def
23
+ return node unless node.type == :def or node.type == :deff
24
24
  return node if [:constructor, :initialize].include?(node.children.first)
25
25
 
26
26
  children = node.children[1..-1]
@@ -30,6 +30,18 @@ module Ruby2JS
30
30
  node.updated nil, [node.children[0], children.first,
31
31
  s(:autoreturn, *children[1..-1])]
32
32
  end
33
+
34
+ def on_deff(node)
35
+ on_def(node)
36
+ end
37
+
38
+ def on_defs(node)
39
+ node = super
40
+ return node unless node.type == :defs
41
+ children = node.children[3..-1]
42
+ children[-1] = s(:nil) if children.last == nil
43
+ node.updated nil, [*node.children[0..2], s(:autoreturn, *children)]
44
+ end
33
45
  end
34
46
 
35
47
  DEFAULTS.push Return
@@ -0,0 +1,185 @@
1
+ #
2
+ require 'ruby2js'
3
+
4
+ module Ruby2JS
5
+ module Filter
6
+ module Stimulus
7
+ include SEXP
8
+ extend SEXP
9
+
10
+ STIMULUS_IMPORT = s(:import,
11
+ [s(:pair, s(:sym, :as), s(:const, nil, :Stimulus)),
12
+ s(:pair, s(:sym, :from), s(:str, "stimulus"))],
13
+ s(:str, '*'))
14
+
15
+ STIMULUS_IMPORT_SKYPACK = s(:import,
16
+ [s(:pair, s(:sym, :as), s(:const, nil, :Stimulus)),
17
+ s(:pair, s(:sym, :from), s(:str, "https://cdn.skypack.dev/stimulus"))],
18
+ s(:str, '*'))
19
+
20
+ def initialize(*args)
21
+ super
22
+ @stim_scope = []
23
+ @stim_subclasses = []
24
+ end
25
+
26
+ def on_module(node)
27
+ save_scope = @stim_scope
28
+ @stim_scope += @namespace.resolve(node.children.first)
29
+ super
30
+ ensure
31
+ @stim_scope = save_scope
32
+ end
33
+
34
+ def on_class(node)
35
+ cname, inheritance, *body = node.children
36
+ return super unless inheritance == s(:const, nil, :Stimulus) or
37
+ inheritance == s(:const, s(:const, nil, :Stimulus), :Controller) or
38
+ inheritance == s(:send, s(:const, nil, :Stimulus), :Controller) or
39
+ @stim_subclasses.include? @namespace.resolve(inheritance)
40
+
41
+ if inheritance == s(:const, nil, :Stimulus)
42
+ node = node.updated(nil, [node.children.first,
43
+ s(:const, s(:const, nil, :Stimulus), :Controller),
44
+ *node.children[2..-1]])
45
+ end
46
+
47
+ @stim_subclasses << @stim_scope + @namespace.resolve(cname)
48
+
49
+ @stim_targets = Set.new
50
+ @stim_values = Set.new
51
+ @stim_classes = Set.new
52
+ stim_walk(node)
53
+
54
+ prepend_list << (@options[:import_from_skypack] ?
55
+ STIMULUS_IMPORT_SKYPACK : STIMULUS_IMPORT)
56
+
57
+ nodes = body
58
+ if nodes.length == 1 and nodes.first&.type == :begin
59
+ nodes = nodes.first.children.dup
60
+ end
61
+
62
+ unless @stim_classes.empty?
63
+ classes = nodes.find_index {|child|
64
+ child.type == :send and child.children[0..1] == [s(:self), :classes=]
65
+ }
66
+
67
+ if classes == nil
68
+ nodes.unshift s(:send, s(:self), :classes=, s(:array, *@stim_classes))
69
+ elsif nodes[classes].children[2].type == :array
70
+ @stim_classes.merge(nodes[classes].children[2].children)
71
+ nodes[classes] = nodes[classes].updated(nil,
72
+ [*nodes[classes].children[0..1], s(:array, *@stim_classes)])
73
+ end
74
+ end
75
+
76
+ unless @stim_values.empty?
77
+ values = nodes.find_index {|child|
78
+ child.type == :send and child.children[0..1] == [s(:self), :values=]
79
+ }
80
+
81
+ if values == nil
82
+ nodes.unshift s(:send, s(:self), :values=, s(:hash,
83
+ *@stim_values.map {|name| s(:pair, name, s(:const, nil, :String))}))
84
+ elsif nodes[values].children[2].type == :hash
85
+ stim_values = @stim_values.map {|name|
86
+ [s(:sym, name.children.first.to_sym), s(:const, nil, :String)]
87
+ }.to_h.merge(
88
+ nodes[values].children[2].children.map {|pair| pair.children}.to_h
89
+ )
90
+
91
+ nodes[values] = nodes[values].updated(nil,
92
+ [*nodes[values].children[0..1], s(:hash,
93
+ *stim_values.map{|name, value| s(:pair, name, value)})])
94
+ end
95
+ end
96
+
97
+ unless @stim_targets.empty?
98
+ targets = nodes.find_index {|child|
99
+ child.type == :send and child.children[0..1] == [s(:self), :targets=]
100
+ }
101
+
102
+ if targets == nil
103
+ nodes.unshift s(:send, s(:self), :targets=, s(:array, *@stim_targets))
104
+ elsif nodes[targets].children[2].type == :array
105
+ @stim_targets.merge(nodes[targets].children[2].children)
106
+ nodes[targets] = nodes[targets].updated(nil,
107
+ [*nodes[targets].children[0..1], s(:array, *@stim_targets)])
108
+ end
109
+ end
110
+
111
+ props = [:element, :application]
112
+
113
+ props += @stim_targets.map do |name|
114
+ name = name.children.first
115
+ ["#{name}Target", "#{name}Targets", "has#{name[0].upcase}#{name[1..-1]}Target"]
116
+ end
117
+
118
+ props += @stim_values.map do |name|
119
+ name = name.children.first
120
+ ["#{name}Value", "has#{name[0].upcase}#{name[1..-1]}Value"]
121
+ end
122
+
123
+ props += @stim_classes.map do |name|
124
+ name = name.children.first
125
+ ["#{name}Class", "has#{name[0].upcase}#{name[1..-1]}Class"]
126
+ end
127
+
128
+ props = props.flatten.map {|prop| [prop.to_sym, s(:self)]}.to_h
129
+
130
+ props[:initialize] = s(:autobind, s(:self))
131
+
132
+ nodes.unshift s(:defineProps, props)
133
+
134
+ nodes.pop unless nodes.last
135
+
136
+ node.updated(nil, [*node.children[0..1], s(:begin, *process_all(nodes))])
137
+ end
138
+
139
+ # analyze ivar usage
140
+ def stim_walk(node)
141
+ node.children.each do |child|
142
+ next unless Parser::AST::Node === child
143
+ stim_walk(child)
144
+
145
+ if child.type == :send and child.children.length == 2 and
146
+ [nil, s(:self), s(:send, nil, :this)].include? child.children[0]
147
+
148
+ if child.children[1] =~ /^has([A-Z]\w*)(Target|Value|Class)$/
149
+ name = s(:str, $1[0].downcase + $1[1..-1])
150
+ @stim_targets.add name if $2 == 'Target'
151
+ @stim_values.add name if $2 == 'Value'
152
+ @stim_classes.add name if $2 == 'Class'
153
+ elsif child.children[1] =~ /^(\w+)Targets?$/
154
+ @stim_targets.add s(:str, $1)
155
+ elsif child.children[1] =~ /^(\w+)Value=?$/
156
+ @stim_values.add s(:str, $1)
157
+ elsif child.children[1] =~ /^(\w+)Class$/
158
+ @stim_classes.add s(:str, $1)
159
+ end
160
+
161
+ elsif child.type == :send and child.children.length == 3 and
162
+ [s(:self), s(:send, nil, :this)].include? child.children[0]
163
+
164
+ if child.children[1] =~ /^(\w+)Value=$/
165
+ @stim_values.add s(:str, $1)
166
+ end
167
+
168
+ elsif child.type == :lvasgn
169
+ if child.children[0] =~ /^(\w+)Value$/
170
+ @stim_values.add s(:str, $1)
171
+ end
172
+
173
+ elsif child.type == :def
174
+ if child.children[0] =~ /^(\w+)ValueChanged$/
175
+ @stim_values.add s(:str, $1)
176
+ end
177
+ end
178
+
179
+ end
180
+ end
181
+ end
182
+
183
+ DEFAULTS.push Stimulus
184
+ end
185
+ end
@@ -0,0 +1,291 @@
1
+ # convert a JSX expression into wunderbar statements
2
+ #
3
+ # Once the syntax is converted to pure Ruby statements,
4
+ # it can then be converted into either React or Vue
5
+ # rendering instructions.
6
+
7
+ module Ruby2JS
8
+ def self.jsx2_rb(string)
9
+ JsxParser.new(string.chars.each).parse.join("\n")
10
+ end
11
+
12
+ class JsxParser
13
+ def initialize(stream)
14
+ @stream = stream
15
+ @state = :text
16
+ @text = ''
17
+ @result = []
18
+ @element = ''
19
+ @attrs = {}
20
+ @attr_name = ''
21
+ @value = ''
22
+ @tag_stack = []
23
+ @expr_nesting = 0
24
+ @wrap_value = true
25
+ end
26
+
27
+ def parse(state = :text, wrap_value = true)
28
+ @wrap_value = wrap_value
29
+ @state = state
30
+ backtrace = ''
31
+ prev = nil
32
+
33
+ loop do
34
+ c = @stream.next
35
+
36
+ if c == "\n"
37
+ backtrace = ''
38
+ else
39
+ backtrace += c
40
+ end
41
+
42
+ case @state
43
+ when :text
44
+ if c == '<'
45
+ @result << "_(\"#{@text.strip}\")" unless @text.strip.empty?
46
+ if @tag_stack.empty?
47
+ @result += self.class.new(@stream).parse(:element)
48
+ @state = :text
49
+ @text = ''
50
+ else
51
+ @state = :element
52
+ @element = ''
53
+ @attrs = {}
54
+ end
55
+ elsif c == '\\'
56
+ @text += c + c
57
+ elsif c == '{'
58
+ @result << "_(\"#{@text}\")" unless @text.empty?
59
+ @result += parse_expr
60
+ @text = ''
61
+ else
62
+ @text += c unless @text.empty? and c =~ /\s/
63
+ end
64
+
65
+ when :element
66
+ if c == '/'
67
+ if @element == ''
68
+ @state = :close
69
+ @element = ''
70
+ else
71
+ @state = :void
72
+ end
73
+ elsif c == '>'
74
+ @result << "_#{@element} do"
75
+ @tag_stack << @element
76
+ @state = :text
77
+ @text = ''
78
+ elsif c == ' '
79
+ @state = :attr_name
80
+ @attr_name = ''
81
+ @attrs = {}
82
+ elsif c == '-'
83
+ @element += '_'
84
+ elsif c =~ /^\w$/
85
+ @element += c
86
+ else
87
+ raise SyntaxError.new("invalid character in element name: #{c.inspect}")
88
+ end
89
+
90
+ when :close
91
+ if c == '>'
92
+ if @element == @tag_stack.last
93
+ @tag_stack.pop
94
+ elsif @tag_stack.last
95
+ raise SyntaxError.new("missing close tag for: #{@tag_stack.last.inspect}")
96
+ else
97
+ raise SyntaxError.new("close tag for element that is not open: #{@element}")
98
+ end
99
+
100
+ @result << 'end'
101
+ return @result if @tag_stack.empty?
102
+ elsif c =~ /^\w$/
103
+ @element += c
104
+ elsif c != ' '
105
+ raise SyntaxError.new("invalid character in element: #{c.inspect}")
106
+ end
107
+
108
+ when :void
109
+ if c == '>'
110
+ if @attrs.empty?
111
+ @result << "_#{@element}"
112
+ else
113
+ @result << "_#{@element}(#{@attrs.map {|name, value| "#{name}: #{value}"}.join(' ')})"
114
+ end
115
+ return @result if @tag_stack.empty?
116
+
117
+ @state = :text
118
+ @text = ''
119
+ elsif c != ' '
120
+ raise SyntaxError.new('invalid character in element: "/"')
121
+ end
122
+
123
+ when :attr_name
124
+ if c =~ /^\w$/
125
+ @attr_name += c
126
+ elsif c == '='
127
+ @state = :attr_value
128
+ @value = ''
129
+ elsif c == '/' and @attr_name == ''
130
+ @state = :void
131
+ elsif c == ' ' or c == "\n" or c == '>'
132
+ if not @attr_name.empty?
133
+ raise SyntaxError.new("missing \"=\" after attribute #{@attr_name.inspect} " +
134
+ "in element #{@element.inspect}")
135
+ elsif c == '>'
136
+ @result << "_#{@element}(#{@attrs.map {|name, value| "#{name}: #{value}"}.join(' ')}) do"
137
+ @tag_stack << @element
138
+ @state = :text
139
+ @text = ''
140
+ end
141
+ else
142
+ raise SyntaxError.new("invalid character in attribute name: #{c.inspect}")
143
+ end
144
+
145
+ when :attr_value
146
+ if c == '"'
147
+ @state = :dquote
148
+ elsif c == "'"
149
+ @state = :squote
150
+ elsif c == '{'
151
+ @attrs[@attr_name] = parse_value
152
+ @state = :attr_name
153
+ @attr_name = ''
154
+ else
155
+ raise SyntaxError.new("invalid value for attribute #{@attr_name.inspect} " +
156
+ "in element #{@element.inspect}")
157
+ end
158
+
159
+ when :dquote
160
+ if c == '"'
161
+ @attrs[@attr_name] = '"' + @value + '"'
162
+ @state = :attr_name
163
+ @attr_name = ''
164
+ elsif c == "\\"
165
+ @value += c + c
166
+ else
167
+ @value += c
168
+ end
169
+
170
+ when :squote
171
+ if c == "'"
172
+ @attrs[@attr_name] = "'" + @value + "'"
173
+ @state = :attr_name
174
+ @attr_name = ''
175
+ elsif c == "\\"
176
+ @value += c + c
177
+ else
178
+ @value += c
179
+ end
180
+
181
+ when :expr
182
+ if c == "}"
183
+ if @expr_nesting > 0
184
+ @value += c
185
+ @expr_nesting -= 1
186
+ else
187
+ @result << (@wrap_value ? "_(#{@value})" : @value)
188
+ return @result
189
+ end
190
+ elsif c == '<'
191
+ if prev =~ /[\w\)\]\}]/
192
+ @value += c # less than
193
+ elsif prev == ' '
194
+ if @stream.peek =~ /[a-zA-Z]/
195
+ @value += parse_element.join(';')
196
+ @wrap_value = false
197
+ else
198
+ @value += c
199
+ end
200
+ else
201
+ @value += parse_element.join(';')
202
+ @wrap_value = false
203
+ end
204
+ else
205
+ @value += c
206
+ @state = :expr_squote if c == "'"
207
+ @state = :expr_dquote if c == '"'
208
+ @expr_nesting += 1 if c == '{'
209
+ end
210
+
211
+ when :expr_squote
212
+ @value += c
213
+ if c == "\\"
214
+ @state = :expr_squote_backslash
215
+ elsif c == "'"
216
+ @state = :expr
217
+ end
218
+
219
+ when :expr_squote_backslash
220
+ @value += c
221
+ @state = :expr_squote
222
+
223
+ when :expr_dquote
224
+ @value += c
225
+ if c == "\\"
226
+ @state = :expr_dquote_backslash
227
+ elsif c == '#'
228
+ @state = :expr_dquote_hash
229
+ elsif c == '"'
230
+ @state = :expr
231
+ end
232
+
233
+ when :expr_dquote_backslash
234
+ @value += c
235
+ @state = :expr_dquote
236
+
237
+ when :expr_dquote_hash
238
+ @value += c
239
+ @value += parse_value + '}' if c == '{'
240
+ @state = :expr_dquote
241
+
242
+ else
243
+ raise RangeError.new("internal state error in JSX: #{@state.inspect}")
244
+ end
245
+
246
+ prev = c
247
+ end
248
+
249
+ unless @tag_stack.empty?
250
+ raise SyntaxError.new("missing close tag for: #{@tag_stack.last.inspect}")
251
+ end
252
+
253
+ case @state
254
+ when :text
255
+ @result << "_(\"#{@text.strip}\")" unless @text.strip.empty?
256
+
257
+ when :element, :attr_name, :attr_value
258
+ raise SyntaxError.new("unclosed element #{@element.inspect}")
259
+
260
+ when :dquote, :squote, :expr_dquote, :expr_dquote_backslash,
261
+ :expr_squote, :expr_squote_backslash
262
+ raise SyntaxError.new("unclosed quote")
263
+
264
+ when :expr
265
+ raise SyntaxError.new("unclosed value")
266
+
267
+ else
268
+ raise RangeError.new("internal state error in JSX: #{@state.inspect}")
269
+ end
270
+
271
+ @result
272
+ rescue SyntaxError => e
273
+ e.set_backtrace backtrace
274
+ raise e
275
+ end
276
+
277
+ private
278
+
279
+ def parse_value
280
+ self.class.new(@stream).parse(:expr, false).join(',')
281
+ end
282
+
283
+ def parse_expr
284
+ self.class.new(@stream).parse(:expr, true)
285
+ end
286
+
287
+ def parse_element
288
+ self.class.new(@stream).parse(:element)
289
+ end
290
+ end
291
+ end