ruby2js 3.5.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -662
  3. data/lib/ruby2js.rb +61 -10
  4. data/lib/ruby2js/converter.rb +10 -4
  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 +102 -31
  10. data/lib/ruby2js/converter/def.rb +7 -3
  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 +35 -4
  16. data/lib/ruby2js/converter/kwbegin.rb +9 -2
  17. data/lib/ruby2js/converter/literal.rb +14 -2
  18. data/lib/ruby2js/converter/module.rb +41 -4
  19. data/lib/ruby2js/converter/opasgn.rb +8 -0
  20. data/lib/ruby2js/converter/send.rb +45 -5
  21. data/lib/ruby2js/converter/vasgn.rb +5 -0
  22. data/lib/ruby2js/converter/xstr.rb +1 -1
  23. data/lib/ruby2js/demo.rb +53 -0
  24. data/lib/ruby2js/es2022.rb +5 -0
  25. data/lib/ruby2js/es2022/strict.rb +3 -0
  26. data/lib/ruby2js/filter.rb +9 -1
  27. data/lib/ruby2js/filter/active_functions.rb +44 -0
  28. data/lib/ruby2js/filter/camelCase.rb +4 -3
  29. data/lib/ruby2js/filter/cjs.rb +2 -0
  30. data/lib/ruby2js/filter/esm.rb +133 -7
  31. data/lib/ruby2js/filter/functions.rb +107 -98
  32. data/lib/ruby2js/filter/{wunderbar.rb → jsx.rb} +29 -7
  33. data/lib/ruby2js/filter/node.rb +95 -74
  34. data/lib/ruby2js/filter/nokogiri.rb +15 -41
  35. data/lib/ruby2js/filter/react.rb +191 -56
  36. data/lib/ruby2js/filter/require.rb +100 -5
  37. data/lib/ruby2js/filter/return.rb +15 -1
  38. data/lib/ruby2js/filter/securerandom.rb +33 -0
  39. data/lib/ruby2js/filter/stimulus.rb +185 -0
  40. data/lib/ruby2js/filter/vue.rb +9 -0
  41. data/lib/ruby2js/jsx.rb +291 -0
  42. data/lib/ruby2js/namespace.rb +75 -0
  43. data/lib/ruby2js/rails.rb +15 -9
  44. data/lib/ruby2js/serializer.rb +3 -1
  45. data/lib/ruby2js/version.rb +3 -3
  46. data/ruby2js.gemspec +1 -1
  47. metadata +14 -5
  48. data/lib/ruby2js/filter/esm_migration.rb +0 -72
  49. data/lib/ruby2js/filter/fast-deep-equal.rb +0 -23
@@ -20,7 +20,9 @@ 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
+ return node if [:constructor, :initialize].include?(node.children.first)
25
+
24
26
  children = node.children[1..-1]
25
27
 
26
28
  children[-1] = s(:nil) if children.last == nil
@@ -28,6 +30,18 @@ module Ruby2JS
28
30
  node.updated nil, [node.children[0], children.first,
29
31
  s(:autoreturn, *children[1..-1])]
30
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
31
45
  end
32
46
 
33
47
  DEFAULTS.push Return
@@ -0,0 +1,33 @@
1
+ require 'ruby2js'
2
+ require 'set'
3
+
4
+ # Experimental secure random support
5
+
6
+ module Ruby2JS
7
+ module Filter
8
+ module SecureRandom
9
+ include SEXP
10
+ extend SEXP
11
+
12
+ IMPORT_BASE62_RANDOM = s(:import, ['base62-random'],
13
+ s(:attr, nil, :base62_random))
14
+
15
+ def on_send(node)
16
+ target, method, *args = node.children
17
+
18
+ if target == s(:const, nil, :SecureRandom)
19
+ if method == :alphanumeric and args.length == 1
20
+ prepend_list << IMPORT_BASE62_RANDOM
21
+ node.updated(nil, [nil, :base62_random, *args])
22
+ else
23
+ super
24
+ end
25
+ else
26
+ super
27
+ end
28
+ end
29
+ end
30
+
31
+ DEFAULTS.push SecureRandom
32
+ end
33
+ end
@@ -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
@@ -106,6 +106,7 @@ module Ruby2JS
106
106
  computed = []
107
107
  setters = []
108
108
  options = []
109
+ watch = nil
109
110
  el = nil
110
111
  mixins = []
111
112
 
@@ -205,6 +206,9 @@ module Ruby2JS
205
206
  end
206
207
 
207
208
  @vue_h = args.children.first.children.last
209
+ elsif method == :watch and args.children.length == 0 and block.type == :hash
210
+ watch = process(block)
211
+ next
208
212
  elsif method == :initialize
209
213
  method = :data
210
214
 
@@ -332,6 +336,11 @@ module Ruby2JS
332
336
  hash << s(:pair, s(:sym, :computed), s(:hash, *computed))
333
337
  end
334
338
 
339
+ # append watch to hash
340
+ if watch
341
+ hash << s(:pair, s(:sym, :watch), watch)
342
+ end
343
+
335
344
  # convert class name to camel case
336
345
  cname = cname.children.last
337
346
  camel = cname.to_s.gsub(/[^\w]/, '-').
@@ -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