ruby2js 3.5.1 → 4.0.0

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