ruby2js 3.5.3 → 4.0.2

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -665
  3. data/lib/ruby2js.rb +60 -15
  4. data/lib/ruby2js/converter.rb +39 -3
  5. data/lib/ruby2js/converter/args.rb +6 -1
  6. data/lib/ruby2js/converter/assign.rb +159 -0
  7. data/lib/ruby2js/converter/begin.rb +7 -2
  8. data/lib/ruby2js/converter/case.rb +7 -2
  9. data/lib/ruby2js/converter/class.rb +80 -21
  10. data/lib/ruby2js/converter/class2.rb +107 -33
  11. data/lib/ruby2js/converter/def.rb +7 -3
  12. data/lib/ruby2js/converter/dstr.rb +8 -3
  13. data/lib/ruby2js/converter/for.rb +1 -1
  14. data/lib/ruby2js/converter/hash.rb +28 -6
  15. data/lib/ruby2js/converter/hide.rb +13 -0
  16. data/lib/ruby2js/converter/if.rb +10 -2
  17. data/lib/ruby2js/converter/import.rb +19 -4
  18. data/lib/ruby2js/converter/kwbegin.rb +9 -2
  19. data/lib/ruby2js/converter/literal.rb +14 -2
  20. data/lib/ruby2js/converter/logical.rb +1 -1
  21. data/lib/ruby2js/converter/module.rb +41 -4
  22. data/lib/ruby2js/converter/next.rb +10 -2
  23. data/lib/ruby2js/converter/opasgn.rb +8 -0
  24. data/lib/ruby2js/converter/redo.rb +14 -0
  25. data/lib/ruby2js/converter/return.rb +2 -1
  26. data/lib/ruby2js/converter/send.rb +73 -8
  27. data/lib/ruby2js/converter/vasgn.rb +5 -0
  28. data/lib/ruby2js/converter/while.rb +1 -1
  29. data/lib/ruby2js/converter/whilepost.rb +1 -1
  30. data/lib/ruby2js/converter/xstr.rb +2 -3
  31. data/lib/ruby2js/demo.rb +53 -0
  32. data/lib/ruby2js/es2022.rb +5 -0
  33. data/lib/ruby2js/es2022/strict.rb +3 -0
  34. data/lib/ruby2js/filter.rb +9 -1
  35. data/lib/ruby2js/filter/active_functions.rb +44 -0
  36. data/lib/ruby2js/filter/camelCase.rb +6 -3
  37. data/lib/ruby2js/filter/cjs.rb +2 -0
  38. data/lib/ruby2js/filter/esm.rb +118 -26
  39. data/lib/ruby2js/filter/functions.rb +137 -109
  40. data/lib/ruby2js/filter/{wunderbar.rb → jsx.rb} +29 -7
  41. data/lib/ruby2js/filter/node.rb +58 -14
  42. data/lib/ruby2js/filter/nokogiri.rb +12 -12
  43. data/lib/ruby2js/filter/react.rb +182 -57
  44. data/lib/ruby2js/filter/require.rb +102 -11
  45. data/lib/ruby2js/filter/return.rb +13 -1
  46. data/lib/ruby2js/filter/stimulus.rb +187 -0
  47. data/lib/ruby2js/jsx.rb +309 -0
  48. data/lib/ruby2js/namespace.rb +75 -0
  49. data/lib/ruby2js/serializer.rb +19 -12
  50. data/lib/ruby2js/sprockets.rb +40 -0
  51. data/lib/ruby2js/version.rb +3 -3
  52. data/ruby2js.gemspec +2 -2
  53. metadata +23 -13
  54. data/lib/ruby2js/filter/esm_migration.rb +0 -72
  55. data/lib/ruby2js/rails.rb +0 -63
@@ -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,98 @@ 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
+ importname = "./#{importname}" unless importname.start_with? '.'
115
+
116
+ prepend_list << s(:import, importname, *imports)
117
+
118
+ save_prepend_list = prepend_list.dup
119
+
120
+ begin
121
+ require_relative = @require_relative
122
+ @require_relative = Pathname.new(@require_relative).join(basename).parent.to_s
123
+ node = process s(:hide, ast)
124
+ ensure
125
+ @require_relative = require_relative
126
+ end
127
+
128
+ if @require_recursive
129
+ block = node.children
130
+ while block.length == 1 and block.first.type == :begin
131
+ block = block.first.children
132
+ end
133
+
134
+ block.each do |child|
135
+ if child&.type == :import
136
+ puts ['rr', basename, child.inspect]
137
+ prepend_list << child
138
+ end
139
+ end
140
+ else
141
+ prepend_list.keep_if do |import|
142
+ save_prepend_list.include? import
143
+ end
144
+ end
145
+
146
+ node
147
+ end
52
148
  ensure
53
149
  if file2
54
150
  @options[:file2] = file2
@@ -57,12 +153,7 @@ module Ruby2JS
57
153
  end
58
154
  end
59
155
  else
60
- begin
61
- require_expr, @require_expr = @require_expr, true
62
- super
63
- ensure
64
- @require_expr = require_expr
65
- end
156
+ super
66
157
  end
67
158
  end
68
159
 
@@ -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,187 @@
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
+ if modules_enabled?
55
+ prepend_list << (@options[:import_from_skypack] ?
56
+ STIMULUS_IMPORT_SKYPACK : STIMULUS_IMPORT)
57
+ end
58
+
59
+ nodes = body
60
+ if nodes.length == 1 and nodes.first&.type == :begin
61
+ nodes = nodes.first.children.dup
62
+ end
63
+
64
+ unless @stim_classes.empty?
65
+ classes = nodes.find_index {|child|
66
+ child.type == :send and child.children[0..1] == [s(:self), :classes=]
67
+ }
68
+
69
+ if classes == nil
70
+ nodes.unshift s(:send, s(:self), :classes=, s(:array, *@stim_classes))
71
+ elsif nodes[classes].children[2].type == :array
72
+ @stim_classes.merge(nodes[classes].children[2].children)
73
+ nodes[classes] = nodes[classes].updated(nil,
74
+ [*nodes[classes].children[0..1], s(:array, *@stim_classes)])
75
+ end
76
+ end
77
+
78
+ unless @stim_values.empty?
79
+ values = nodes.find_index {|child|
80
+ child.type == :send and child.children[0..1] == [s(:self), :values=]
81
+ }
82
+
83
+ if values == nil
84
+ nodes.unshift s(:send, s(:self), :values=, s(:hash,
85
+ *@stim_values.map {|name| s(:pair, name, s(:const, nil, :String))}))
86
+ elsif nodes[values].children[2].type == :hash
87
+ stim_values = @stim_values.map {|name|
88
+ [s(:sym, name.children.first.to_sym), s(:const, nil, :String)]
89
+ }.to_h.merge(
90
+ nodes[values].children[2].children.map {|pair| pair.children}.to_h
91
+ )
92
+
93
+ nodes[values] = nodes[values].updated(nil,
94
+ [*nodes[values].children[0..1], s(:hash,
95
+ *stim_values.map{|name, value| s(:pair, name, value)})])
96
+ end
97
+ end
98
+
99
+ unless @stim_targets.empty?
100
+ targets = nodes.find_index {|child|
101
+ child.type == :send and child.children[0..1] == [s(:self), :targets=]
102
+ }
103
+
104
+ if targets == nil
105
+ nodes.unshift s(:send, s(:self), :targets=, s(:array, *@stim_targets))
106
+ elsif nodes[targets].children[2].type == :array
107
+ @stim_targets.merge(nodes[targets].children[2].children)
108
+ nodes[targets] = nodes[targets].updated(nil,
109
+ [*nodes[targets].children[0..1], s(:array, *@stim_targets)])
110
+ end
111
+ end
112
+
113
+ props = [:element, :application]
114
+
115
+ props += @stim_targets.map do |name|
116
+ name = name.children.first
117
+ ["#{name}Target", "#{name}Targets", "has#{name[0].upcase}#{name[1..-1]}Target"]
118
+ end
119
+
120
+ props += @stim_values.map do |name|
121
+ name = name.children.first
122
+ ["#{name}Value", "has#{name[0].upcase}#{name[1..-1]}Value"]
123
+ end
124
+
125
+ props += @stim_classes.map do |name|
126
+ name = name.children.first
127
+ ["#{name}Class", "has#{name[0].upcase}#{name[1..-1]}Class"]
128
+ end
129
+
130
+ props = props.flatten.map {|prop| [prop.to_sym, s(:self)]}.to_h
131
+
132
+ props[:initialize] = s(:autobind, s(:self))
133
+
134
+ nodes.unshift s(:defineProps, props)
135
+
136
+ nodes.pop unless nodes.last
137
+
138
+ node.updated(nil, [*node.children[0..1], s(:begin, *process_all(nodes))])
139
+ end
140
+
141
+ # analyze ivar usage
142
+ def stim_walk(node)
143
+ node.children.each do |child|
144
+ next unless Parser::AST::Node === child
145
+ stim_walk(child)
146
+
147
+ if child.type == :send and child.children.length == 2 and
148
+ [nil, s(:self), s(:send, nil, :this)].include? child.children[0]
149
+
150
+ if child.children[1] =~ /^has([A-Z]\w*)(Target|Value|Class)$/
151
+ name = s(:str, $1[0].downcase + $1[1..-1])
152
+ @stim_targets.add name if $2 == 'Target'
153
+ @stim_values.add name if $2 == 'Value'
154
+ @stim_classes.add name if $2 == 'Class'
155
+ elsif child.children[1] =~ /^(\w+)Targets?$/
156
+ @stim_targets.add s(:str, $1)
157
+ elsif child.children[1] =~ /^(\w+)Value=?$/
158
+ @stim_values.add s(:str, $1)
159
+ elsif child.children[1] =~ /^(\w+)Class$/
160
+ @stim_classes.add s(:str, $1)
161
+ end
162
+
163
+ elsif child.type == :send and child.children.length == 3 and
164
+ [s(:self), s(:send, nil, :this)].include? child.children[0]
165
+
166
+ if child.children[1] =~ /^(\w+)Value=$/
167
+ @stim_values.add s(:str, $1)
168
+ end
169
+
170
+ elsif child.type == :lvasgn
171
+ if child.children[0] =~ /^(\w+)Value$/
172
+ @stim_values.add s(:str, $1)
173
+ end
174
+
175
+ elsif child.type == :def
176
+ if child.children[0] =~ /^(\w+)ValueChanged$/
177
+ @stim_values.add s(:str, $1)
178
+ end
179
+ end
180
+
181
+ end
182
+ end
183
+ end
184
+
185
+ DEFAULTS.push Stimulus
186
+ end
187
+ end
@@ -0,0 +1,309 @@
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.respond_to?(:next) ? stream : OpalEnumerator.new(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
+
292
+ # Opal's enumerator doesn't currently support next and peek methods.
293
+ # Build a wrapper that adds those methods.
294
+ class OpalEnumerator
295
+ def initialize(stream)
296
+ @stream = stream.to_a
297
+ end
298
+
299
+ def next
300
+ raise StopIteration.new if @stream.empty?
301
+ @stream.shift
302
+ end
303
+
304
+ def peek
305
+ raise StopIteration.new if @stream.empty?
306
+ @stream.first
307
+ end
308
+ end
309
+ end