ruby2js 3.5.3 → 4.0.2

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