ruby2js 3.0.15 → 3.3.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 +92 -7
  3. data/lib/ruby2js.rb +30 -3
  4. data/lib/ruby2js/converter.rb +23 -3
  5. data/lib/ruby2js/converter/args.rb +31 -1
  6. data/lib/ruby2js/converter/block.rb +2 -2
  7. data/lib/ruby2js/converter/casgn.rb +3 -2
  8. data/lib/ruby2js/converter/class.rb +2 -2
  9. data/lib/ruby2js/converter/class2.rb +117 -13
  10. data/lib/ruby2js/converter/cvar.rb +5 -3
  11. data/lib/ruby2js/converter/cvasgn.rb +5 -3
  12. data/lib/ruby2js/converter/def.rb +2 -2
  13. data/lib/ruby2js/converter/for.rb +8 -1
  14. data/lib/ruby2js/converter/hash.rb +22 -6
  15. data/lib/ruby2js/converter/if.rb +13 -2
  16. data/lib/ruby2js/converter/import.rb +38 -0
  17. data/lib/ruby2js/converter/ivar.rb +2 -0
  18. data/lib/ruby2js/converter/ivasgn.rb +1 -1
  19. data/lib/ruby2js/converter/kwbegin.rb +16 -2
  20. data/lib/ruby2js/converter/logical.rb +46 -1
  21. data/lib/ruby2js/converter/opasgn.rb +5 -2
  22. data/lib/ruby2js/converter/regexp.rb +27 -2
  23. data/lib/ruby2js/converter/return.rb +1 -1
  24. data/lib/ruby2js/converter/send.rb +160 -69
  25. data/lib/ruby2js/converter/super.rb +15 -9
  26. data/lib/ruby2js/converter/xnode.rb +89 -0
  27. data/lib/ruby2js/es2018.rb +5 -0
  28. data/lib/ruby2js/es2018/strict.rb +3 -0
  29. data/lib/ruby2js/es2019.rb +5 -0
  30. data/lib/ruby2js/es2019/strict.rb +3 -0
  31. data/lib/ruby2js/es2020.rb +5 -0
  32. data/lib/ruby2js/es2020/strict.rb +3 -0
  33. data/lib/ruby2js/es2021.rb +5 -0
  34. data/lib/ruby2js/es2021/strict.rb +3 -0
  35. data/lib/ruby2js/filter.rb +1 -0
  36. data/lib/ruby2js/filter/cjs.rb +2 -2
  37. data/lib/ruby2js/filter/esm.rb +72 -0
  38. data/lib/ruby2js/filter/functions.rb +218 -34
  39. data/lib/ruby2js/filter/matchAll.rb +49 -0
  40. data/lib/ruby2js/filter/node.rb +18 -10
  41. data/lib/ruby2js/filter/nokogiri.rb +13 -13
  42. data/lib/ruby2js/filter/react.rb +190 -30
  43. data/lib/ruby2js/filter/require.rb +1 -4
  44. data/lib/ruby2js/filter/rubyjs.rb +4 -4
  45. data/lib/ruby2js/filter/vue.rb +45 -17
  46. data/lib/ruby2js/filter/wunderbar.rb +63 -0
  47. data/lib/ruby2js/serializer.rb +25 -11
  48. data/lib/ruby2js/version.rb +2 -2
  49. metadata +16 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7acab4a28c4ec163da40b2f1847692df089ff593af8d735bf163cfe566ed7f5b
4
- data.tar.gz: f0f33e2c6beba7785d535ece64e4402cb5aac29fca9f9541087857905ba33d8f
3
+ metadata.gz: b2ad87139fac25c7b097f2ee32c59b141936bb8424cf93a4a20c2d9393495528
4
+ data.tar.gz: 4de6a0b69d4d496faae496b37c21c66caee4f3793b1241baf134b5a05c85c7f9
5
5
  SHA512:
6
- metadata.gz: d53db4145f632be93c234cf706b25e6c7c27a0d0c1a864543045e6d9a9672e9e9792281677da6a4b107a3eac0d173ca2cd151a0d0f91acf3171ecc5a84dd06c4
7
- data.tar.gz: 49364f4bb8cf3f05ea9d07664d800a86af81817b096a40c89ec8848a6f45fe62b755cc2e0c995ce49f300bb84a6caa1de4b5fd822b8ae403579e9244dde9e442
6
+ metadata.gz: 8e359e299801b09bc428a93fdf95d4ac1128fbb19266636034fad929d3fa3759f504e59bbd70eb923733b0656770309f845e23aeb0e4e24424ab0161f6fe528a
7
+ data.tar.gz: '0874df3f8606f65be7cbae4114c2d384938f7827391020ee0f85d0d99340e4e905010dbe7216ebab17f0adf72fa7793525c95a6e083273abc08cae3dd076d849'
data/README.md CHANGED
@@ -92,6 +92,18 @@ Enable strict support:
92
92
  puts Ruby2JS.convert('a=1', strict: true)
93
93
  ```
94
94
 
95
+ Emit strict equality comparisons:
96
+
97
+ ```ruby
98
+ puts Ruby2JS.convert('a==1', comparison: :identity)
99
+ ```
100
+
101
+ Emit nullish coalescing operators:
102
+
103
+ ```ruby
104
+ puts Ruby2JS.convert('a || 1', or: :nullish)
105
+ ```
106
+
95
107
  With [ExecJS](https://github.com/sstephenson/execjs):
96
108
  ```ruby
97
109
  require 'ruby2js/execjs'
@@ -220,9 +232,10 @@ the script.
220
232
  * `.clear` becomes `.length = 0`
221
233
  * `.delete` becomes `delete target[arg]`
222
234
  * `.downcase` becomes `.toLowerCase`
223
- * `.each` becomes `for (i in ...) {}`
224
- * `.each_key` becomes `Object.keys().forEach`
225
- * `.each_value` becomes `for (i in ...) {}`
235
+ * `.each` becomes `.forEach`
236
+ * `.each_key` becomes `for (i in ...) {}`
237
+ * `.each_pair` becomes `for (var key in item) {var value = item[key]; ...}`
238
+ * `.each_value` becomes `.forEach`
226
239
  * `.each_with_index` becomes `.forEach`
227
240
  * `.end_with?` becomes `.slice(-arg.length) == arg`
228
241
  * `.empty?` becomes `.length == 0`
@@ -232,10 +245,12 @@ the script.
232
245
  * `.gsub` becomes `replace(//g)`
233
246
  * `.include?` becomes `.indexOf() != -1`
234
247
  * `.inspect` becomes `JSON.stringify()`
235
- * `.keys` becomes `Object.keys()`
248
+ * `.keys()` becomes `Object.keys()`
236
249
  * `.last` becomes `[*.length-1]`
237
250
  * `.last(n)` becomes `.slice(*.length-1, *.length)`
251
+ * `.lstrip` becomes `.replace(/^\s+/, "")`
238
252
  * `.max` becomes `Math.max.apply(Math)`
253
+ * `.merge` becomes `Object.assign({}, ...)`
239
254
  * `.merge!` becomes `Object.assign()`
240
255
  * `.min` becomes `Math.min.apply(Math)`
241
256
  * `.nil?` becomes `== null`
@@ -243,12 +258,16 @@ the script.
243
258
  * `puts` becomes `console.log`
244
259
  * `.replace` becomes `.length = 0; ...push.apply(*)`
245
260
  * `.respond_to?` becomes `right in left`
261
+ * `.rstrip` becomes `.replace(/s+$/, "")`
246
262
  * `.scan` becomes `.match(//g)`
247
263
  * `.start_with?` becomes `.substring(0, arg.length) == arg`
248
264
  * `.upto(lim)` becomes `for (var i=num; i<=lim; i+=1)`
249
265
  * `.downto(lim)` becomes `for (var i=num; i>=lim; i-=1)`
250
266
  * `.step(lim, n).each` becomes `for (var i=num; i<=lim; i+=n)`
251
267
  * `.step(lim, -n).each` becomes `for (var i=num; i>=lim; i-=n)`
268
+ * `(0..a).to_a` becomes `Array.apply(null, {length: a}).map(Function.call, Number)`
269
+ * `(b..a).to_a` becomes `Array.apply(null, {length: (a-b+1)}).map(Function.call, Number).map(function (idx) { return idx+b })`
270
+ * `(b...a).to_a` becomes `Array.apply(null, {length: (a-b)}).map(Function.call, Number).map(function (idx) { return idx+b })`
252
271
  * `.strip` becomes `.trim`
253
272
  * `.sub` becomes `.replace`
254
273
  * `.to_f` becomes `parseFloat`
@@ -259,6 +278,7 @@ the script.
259
278
  * `[n...m]` becomes `.slice(n,m)`
260
279
  * `[n..m]` becomes `.slice(n,m+1)`
261
280
  * `[/r/, n]` becomes `.match(/r/)[n]`
281
+ * `[/r/, n]=` becomes `.replace(/r/, ...)`
262
282
  * `(1..2).each {|i| ...}` becomes `for (var i=1 i<=2; i+=1)`
263
283
  * `"string" * length` becomes `new Array(length + 1).join("string")`
264
284
  * `.sub!` and `.gsub!` become equivalent `x = x.replace` statements
@@ -276,13 +296,14 @@ the script.
276
296
  * New classes subclassed off of `Exception` will become subclassed off
277
297
  of `Error` instead; and default constructors will be provided
278
298
  * `loop do...end` will be replaced with `while (true) {...}`
299
+ * `raise Exception.new(...)` will be replaced with `throw new Error(...)`
279
300
 
280
301
  Additionally, there is one mapping that will only be done if explicitly
281
302
  <a href="https://github.com/rubys/ruby2js/blob/master/lib/ruby2js/filter.rb">included</a>:
282
303
 
283
304
  * `.class` becomes `.constructor`
284
305
 
285
- * <a id="node" * href="https://github.com/rubys/ruby2js/blob/master/spec/node_spec.rb">node</a>
306
+ * <a id="node" href="https://github.com/rubys/ruby2js/blob/master/spec/node_spec.rb">node</a>
286
307
 
287
308
  * `` `command` `` becomes `child_process.execSync("command", {encoding: "utf8"})`
288
309
  * `ARGV` becomes `process.argv.slice(2)`
@@ -511,6 +532,16 @@ the script.
511
532
  * maps `export default async proc` to `module.exports = async`
512
533
  * maps `export default` to `module.exports =`
513
534
 
535
+ * <a id="matchAll" href="https://github.com/rubys/ruby2js/blob/master/spec/matchAll">matchAll</a>
536
+
537
+ For ES level < 2020:
538
+
539
+ * maps `str.matchAll(pattern).forEach {}` to
540
+ `while (match = pattern.exec(str)) {}`
541
+
542
+ Note `pattern` must be a simple variable with a value of a regular
543
+ expression with the `g` flag set at runtime.
544
+
514
545
  [Wunderbar](https://github.com/rubys/wunderbar) includes additional demos:
515
546
 
516
547
  * [chat](https://github.com/rubys/wunderbar/blob/master/demo/chat.rb),
@@ -532,13 +563,16 @@ conversions are made:
532
563
  * `def f(a, (foo, *bar))` becomes `function f(a, [foo, ...bar])`
533
564
  * `def a(b=1)` becomes `function a(b=1)`
534
565
  * `def a(*b)` becomes `function a(...b)`
535
- * `.each_key` becomes `for (i of ...) {}`
566
+ * `.each_value` becomes `for (i of ...) {}`
536
567
  * `a(*b)` becomes `a(...b)`
537
568
  * `"#{a}"` becomes <code>\`${a}\`</code>
538
569
  * `lambda {|x| x}` becomes `(x) => {return x}`
539
570
  * `proc {|x| x}` becomes `(x) => {x}`
540
571
  * `a {|x|}` becomes `a((x) => {})`
541
572
  * `class Person; end` becomes `class Person {}`
573
+ * `(0...a).to_a` becomes `[...Array(a).keys()]`
574
+ * `(0..a).to_a` becomes `[...Array(a+1).keys()]`
575
+ * `(b..a).to_a` becomes `Array.from({length: (a-b+1)}, (_, idx) => idx+b)`
542
576
 
543
577
  ES2015 class support includes constructors, super, methods, class methods,
544
578
  instance methods, instance variables, class variables, getters, setters,
@@ -549,6 +583,9 @@ Additionally, the `functions` filter will provide the following conversion:
549
583
  * `Array(x)` becomes `Array.from(x)`
550
584
  * `.inject(n) {}` becomes `.reduce(() => {}, n)`
551
585
 
586
+ Finally, keyword arguments and optional keyword arguments will be mapped to
587
+ parameter detructuring.
588
+
552
589
  ES2016 support
553
590
  ---
554
591
 
@@ -562,9 +599,57 @@ ES2017 support
562
599
  ---
563
600
 
564
601
  When option `eslevel: 2017` is provided, the following additional
602
+ conversions are made by the `functions` filter:
603
+
604
+ * `.values()` becomes `Object.values()`
605
+ * `.entries()` becomes `Object.entries()`
606
+ * `.each_pair {}` becomes `for (let [key, value] of Object.entries()) {}'
607
+
608
+ ES2018 support
609
+ ---
610
+
611
+ When option `eslevel: 2018` is provided, the following additional
565
612
  conversion is made by the `functions` filter:
566
613
 
567
- * `.each_entry` becomes `Object.entries().forEach`
614
+ * `.merge` becomes `{...a, ...b}`
615
+
616
+ Additionally, rest arguments can now be used with keyword arguments and
617
+ optional keyword arguments.
618
+
619
+ ES2019 support
620
+ ---
621
+
622
+ When option `eslevel: 2019` is provided, the following additional
623
+ conversion is made by the `functions` filter:
624
+
625
+ * `.flatten` becomes `.flat(Infinity)`
626
+ * `.lstrip` becomes `.trimEnd
627
+ * `.rstrip` becomes `.trimStart
628
+ * `a.to_h` becomes `Object.fromEntries(a)`
629
+ * `Hash[a]` becomes `Object.fromEntries(a)`
630
+
631
+ Additionally, `rescue` without a variable will map to `catch` without a
632
+ variable.
633
+
634
+ ES2020 support
635
+ ---
636
+
637
+ When option `eslevel: 2020` is provided, the following additional
638
+ conversions are made:
639
+
640
+ * `@x` becomes `this.#x`
641
+ * `@@x` becomes `ClassName.#x`
642
+ * `a&.b` becomes `a?.b`
643
+ * `.scan` becomes `Array.from(str.matchAll(/.../g), s => s.slice(1))`
644
+
645
+ ES2021 support
646
+ ---
647
+
648
+ When option `eslevel: 2021` is provided, the following additional
649
+ conversions are made:
650
+
651
+ * `x ||= 1` becomes `x ||= 1`
652
+ * `x &&= 1` becomes `x &&= 1`
568
653
 
569
654
  Picking a Ruby to JS mapping tool
570
655
  ---
@@ -55,6 +55,7 @@ module Ruby2JS
55
55
  def initialize(comments)
56
56
  @comments = comments
57
57
  @ast = nil
58
+ @exclude_methods = []
58
59
  end
59
60
 
60
61
  def options=(options)
@@ -81,6 +82,22 @@ module Ruby2JS
81
82
  @options[:eslevel] >= 2017
82
83
  end
83
84
 
85
+ def es2018
86
+ @options[:eslevel] >= 2018
87
+ end
88
+
89
+ def es2019
90
+ @options[:eslevel] >= 2019
91
+ end
92
+
93
+ def es2020
94
+ @options[:eslevel] >= 2020
95
+ end
96
+
97
+ def es2021
98
+ @options[:eslevel] >= 2021
99
+ end
100
+
84
101
  def process(node)
85
102
  ast, @ast = @ast, node
86
103
  replacement = super
@@ -114,6 +131,9 @@ module Ruby2JS
114
131
  def on_sendw(node); on_send(node); end
115
132
  def on_undefined?(node); on_defined?(node); end
116
133
  def on_nil(node); end
134
+ def on_xnode(node); end
135
+ def on_export(node); end
136
+ def on_import(node); end
117
137
 
118
138
  # provide a method so filters can call 'super'
119
139
  def on_sym(node); node; end
@@ -143,7 +163,7 @@ module Ruby2JS
143
163
 
144
164
  if Proc === source
145
165
  file,line = source.source_location
146
- source = File.read(file.dup.untaint).untaint
166
+ source = IO.read(file)
147
167
  ast, comments = parse(source)
148
168
  comments = Parser::Source::Comment.associate(ast, comments) if ast
149
169
  ast = find_block( ast, line )
@@ -156,9 +176,12 @@ module Ruby2JS
156
176
  comments = Parser::Source::Comment.associate(ast, comments) if ast
157
177
  end
158
178
 
159
- filters = options[:filters] || Filter::DEFAULTS
179
+ filters = (options[:filters] || Filter::DEFAULTS)
160
180
 
161
181
  unless filters.empty?
182
+ filters.dup.each do |filter|
183
+ filters = filter.reorder(filters) if filter.respond_to? :reorder
184
+ end
162
185
 
163
186
  filter = Filter::Processor
164
187
  filters.reverse.each do |mod|
@@ -176,6 +199,8 @@ module Ruby2JS
176
199
  ruby2js.ivars = options[:ivars]
177
200
  ruby2js.eslevel = options[:eslevel]
178
201
  ruby2js.strict = options[:strict]
202
+ ruby2js.comparison = options[:comparison] || :equality
203
+ ruby2js.or = options[:or] || :logical
179
204
  if ruby2js.binding and not ruby2js.ivars
180
205
  ruby2js.ivars = ruby2js.binding.eval \
181
206
  'Hash[instance_variables.map {|var| [var, instance_variable_get(var)]}]'
@@ -201,7 +226,9 @@ module Ruby2JS
201
226
  buffer.source = source.encode('utf-8')
202
227
  parser = Parser::CurrentRuby.new
203
228
  parser.builder.emit_file_line_as_literals=false
204
- parser.parse_with_comments(buffer)
229
+ ast, comments = parser.parse_with_comments(buffer)
230
+ Parser::CurrentRuby.parse(source.encode('utf-8')) unless ast
231
+ [ast, comments]
205
232
  rescue Parser::SyntaxError => e
206
233
  split = source[0..e.diagnostic.location.begin_pos].split("\n")
207
234
  line, col = split.length, split.last.length
@@ -28,7 +28,7 @@ module Ruby2JS
28
28
  :=== => :'!=='
29
29
  }
30
30
 
31
- GROUP_OPERATORS = [:begin, :dstr, :dsym, :and, :or, :casgn]
31
+ GROUP_OPERATORS = [:begin, :dstr, :dsym, :and, :or, :casgn, :if]
32
32
 
33
33
  VASGN = [:cvasgn, :ivasgn, :gvasgn, :lvasgn]
34
34
 
@@ -57,9 +57,12 @@ module Ruby2JS
57
57
  @prototype = nil
58
58
  @class_parent = nil
59
59
  @class_name = nil
60
+ @jsx = false
60
61
 
61
62
  @eslevel = :es5
62
63
  @strict = false
64
+ @comparison = :equality
65
+ @or = :logical
63
66
  end
64
67
 
65
68
  def width=(width)
@@ -124,7 +127,7 @@ module Ruby2JS
124
127
  Parser::AST::Node.new(type, args)
125
128
  end
126
129
 
127
- attr_accessor :strict, :eslevel
130
+ attr_accessor :strict, :eslevel, :comparison, :or
128
131
 
129
132
  def es2015
130
133
  @eslevel >= 2015
@@ -138,6 +141,22 @@ module Ruby2JS
138
141
  @eslevel >= 2017
139
142
  end
140
143
 
144
+ def es2018
145
+ @eslevel >= 2018
146
+ end
147
+
148
+ def es2019
149
+ @eslevel >= 2019
150
+ end
151
+
152
+ def es2020
153
+ @eslevel >= 2020
154
+ end
155
+
156
+ def es2021
157
+ @eslevel >= 2021
158
+ end
159
+
141
160
  @@handlers = []
142
161
  def self.handle(*types, &block)
143
162
  types.each do |type|
@@ -226,7 +245,6 @@ module Ruby2JS
226
245
  if ast.loc and ast.loc.expression
227
246
  filename = ast.loc.expression.source_buffer.name
228
247
  if filename and not filename.empty?
229
- filename = filename.dup.untaint
230
248
  @timestamps[filename] ||= File.mtime(filename)
231
249
  end
232
250
  end
@@ -295,6 +313,7 @@ require 'ruby2js/converter/for'
295
313
  require 'ruby2js/converter/hash'
296
314
  require 'ruby2js/converter/if'
297
315
  require 'ruby2js/converter/in'
316
+ require 'ruby2js/converter/import'
298
317
  require 'ruby2js/converter/ivar'
299
318
  require 'ruby2js/converter/ivasgn'
300
319
  require 'ruby2js/converter/kwbegin'
@@ -321,3 +340,4 @@ require 'ruby2js/converter/vasgn'
321
340
  require 'ruby2js/converter/while'
322
341
  require 'ruby2js/converter/whilepost'
323
342
  require 'ruby2js/converter/xstr'
343
+ require 'ruby2js/converter/xnode'
@@ -7,11 +7,41 @@ module Ruby2JS
7
7
  # (blockarg :c))
8
8
 
9
9
  handle :args do |*args|
10
+ kwargs = []
11
+ while args.last and
12
+ [:kwarg, :kwoptarg, :kwrestarg].include? args.last.type
13
+ kwargs.unshift args.pop
14
+ end
15
+
16
+ if kwargs.length == 1 and kwargs.last.type == :kwrestarg
17
+ args.push s(:arg, *kwargs.last.children)
18
+ end
19
+
20
+ unless kwargs.empty? or es2015
21
+ raise NotImplementedError.new('Keyword args require ES2015')
22
+ end
23
+
10
24
  parse_all(*args, join: ', ')
25
+ if not kwargs.empty?
26
+ put ', ' unless args.empty?
27
+ put '{ '
28
+ kwargs.each_with_index do |kw, index|
29
+ put ', ' unless index == 0
30
+ if kw.type == :kwarg
31
+ put kw.children.first
32
+ elsif kw.type == :kwoptarg
33
+ put kw.children.first; put ' = '; parse kw.children.last
34
+ elsif kw.type == :kwrestarg
35
+ raise 'Rest arg requires ES2018' unless es2018
36
+ put '...'; put kw.children.first
37
+ end
38
+ end
39
+ put ' }'
40
+ end
11
41
  end
12
42
 
13
43
  handle :mlhs do |*args|
14
- if es2015
44
+ if es2015 or @jsx
15
45
  put '['
16
46
  parse_all(*args, join: ', ')
17
47
  put ']'
@@ -9,7 +9,7 @@ module Ruby2JS
9
9
 
10
10
  handle :block do |call, args, block|
11
11
 
12
- if
12
+ if \
13
13
  @state == :statement and args.children.length == 1 and
14
14
  call.children.first and call.children.first.type == :begin and
15
15
  call.children[1] == :step and
@@ -42,7 +42,7 @@ module Ruby2JS
42
42
  @vars = vars if es2015
43
43
  end
44
44
 
45
- elsif
45
+ elsif \
46
46
  call.children[0] == nil and call.children[1] == :function and
47
47
  call.children[2..-1].all? do |child|
48
48
  child.type == :lvar or (child.type == :send and
@@ -8,7 +8,9 @@ module Ruby2JS
8
8
  multi_assign_declarations if @state == :statement
9
9
 
10
10
  begin
11
- if @state == :statement
11
+ cbase ||= @rbstack.map {|rb| rb[var]}.compact.last
12
+
13
+ if @state == :statement and not cbase
12
14
  if es2015
13
15
  put "const "
14
16
  else
@@ -16,7 +18,6 @@ module Ruby2JS
16
18
  end
17
19
  end
18
20
 
19
- cbase ||= @rbstack.map {|rb| rb[var]}.compact.last
20
21
  (parse cbase; put '.') if cbase
21
22
 
22
23
  put "#{ var } = "; parse value
@@ -37,7 +37,7 @@ module Ruby2JS
37
37
  body.compact!
38
38
  visible = {}
39
39
  body.map! do |m|
40
- if
40
+ if \
41
41
  @ast.type == :class_module and m.type == :defs and
42
42
  m.children.first == s(:self)
43
43
  then
@@ -217,7 +217,7 @@ module Ruby2JS
217
217
  end
218
218
 
219
219
  # collapse sequence to a single assignment
220
- if
220
+ if \
221
221
  @ast.type != :class_extend and
222
222
  (methods > 1 or (methods == 1 and body[start].type == :prop))
223
223
  then