ruby2js 3.6.1 → 4.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 (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