slim 1.3.9 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -8
  3. data/.travis.yml +8 -7
  4. data/CHANGES +35 -0
  5. data/Gemfile +9 -9
  6. data/README.md +94 -176
  7. data/Rakefile +7 -14
  8. data/benchmarks/run-benchmarks.rb +9 -37
  9. data/doc/logic_less.md +140 -0
  10. data/doc/translator.md +31 -0
  11. data/lib/slim.rb +3 -1
  12. data/lib/slim/code_attributes.rb +20 -20
  13. data/lib/slim/command.rb +16 -6
  14. data/lib/slim/do_inserter.rb +33 -0
  15. data/lib/slim/embedded.rb +0 -6
  16. data/lib/slim/end_inserter.rb +2 -2
  17. data/lib/slim/engine.rb +9 -23
  18. data/lib/slim/erb_converter.rb +14 -0
  19. data/lib/slim/filter.rb +2 -2
  20. data/lib/slim/logic_less/context.rb +8 -0
  21. data/lib/slim/logic_less/filter.rb +12 -11
  22. data/lib/slim/parser.rb +57 -113
  23. data/lib/slim/splat/builder.rb +79 -0
  24. data/lib/slim/splat/filter.rb +93 -0
  25. data/lib/slim/version.rb +1 -1
  26. data/slim.gemspec +1 -1
  27. data/test/core/helper.rb +1 -3
  28. data/test/core/test_code_blocks.rb +51 -0
  29. data/test/core/test_code_evaluation.rb +4 -12
  30. data/test/core/test_embedded_engines.rb +18 -44
  31. data/test/core/test_encoding.rb +8 -1
  32. data/test/core/test_erb_converter.rb +67 -0
  33. data/test/core/test_html_attributes.rb +19 -25
  34. data/test/core/test_html_escaping.rb +10 -2
  35. data/test/core/test_html_structure.rb +6 -6
  36. data/test/core/test_parser_errors.rb +1 -1
  37. data/test/core/test_ruby_errors.rb +2 -6
  38. data/test/core/test_thread_options.rb +4 -4
  39. data/test/core/test_unicode.rb +18 -0
  40. data/test/literate/TESTS.md +193 -34
  41. data/test/logic_less/test_logic_less.rb +17 -0
  42. data/test/rails/app/controllers/application_controller.rb +0 -1
  43. data/test/rails/app/controllers/entries_controller.rb +5 -0
  44. data/test/rails/app/controllers/slim_controller.rb +3 -0
  45. data/test/rails/app/models/entry.rb +16 -0
  46. data/test/rails/app/views/entries/edit.html.slim +3 -0
  47. data/test/rails/app/views/slim/form_for.html.slim +2 -0
  48. data/test/rails/app/views/slim/xml.slim +1 -0
  49. data/test/rails/config/application.rb +2 -2
  50. data/test/rails/config/environments/test.rb +1 -1
  51. data/test/rails/config/routes.rb +1 -1
  52. data/test/rails/test/helper.rb +0 -3
  53. data/test/rails/test/test_slim.rb +13 -8
  54. data/test/translator/test_translator.rb +13 -2
  55. metadata +17 -14
  56. data/lib/slim/splat_attributes.rb +0 -113
  57. data/test/rails/app/controllers/parents_controller.rb +0 -85
  58. data/test/rails/app/models/child.rb +0 -3
  59. data/test/rails/app/models/parent.rb +0 -4
  60. data/test/rails/app/views/parents/_form.html.slim +0 -8
  61. data/test/rails/app/views/parents/edit.html.slim +0 -2
  62. data/test/rails/app/views/parents/new.html.slim +0 -2
  63. data/test/rails/app/views/parents/show.html.slim +0 -5
  64. data/test/rails/config/database.yml +0 -4
  65. data/test/rails/db/migrate/20101220223037_parents_and_children.rb +0 -17
data/lib/slim/engine.rb CHANGED
@@ -5,37 +5,23 @@ module Slim
5
5
  # This overwrites some Temple default options or sets default options for Slim specific filters.
6
6
  # It is recommended to set the default settings only once in the code and avoid duplication. Only use
7
7
  # `define_options` when you have to override some default settings.
8
- define_options :attr_quote,
9
- :merge_attrs,
10
- :pretty => false,
8
+ define_options :pretty => false,
11
9
  :sort_attrs => true,
12
- :generator => Temple::Generators::ArrayBuffer,
13
- :default_tag => 'div',
14
10
  :attr_quote => '"',
15
11
  :merge_attrs => {'class' => ' '},
16
- :escape_quoted_attrs => false
17
-
18
- # Removed in 2.0
19
- define_deprecated_options :remove_empty_attrs,
20
- :chain,
21
- :escape_quoted_attrs,
22
- :attr_wrapper,
23
- :attr_delimiter
24
-
25
- def initialize(opts = {})
26
- super
27
- deprecated = {}
28
- deprecated[:merge_attrs] = options[:attr_delimiter] if options.include? :attr_delimiter
29
- deprecated[:attr_quote] = options[:attr_wrapper] if options.include? :attr_wrapper
30
- @options = Temple::ImmutableHash.new(deprecated, @options) unless deprecated.empty?
31
- end
12
+ :encoding => 'utf-8',
13
+ :generator => Temple::Generators::ArrayBuffer,
14
+ :default_tag => 'div'
32
15
 
33
- use Slim::Parser, :file, :tabsize, :encoding, :shortcut, :default_tag, :escape_quoted_attrs
16
+ filter :Encoding, :encoding
17
+ filter :RemoveBOM
18
+ use Slim::Parser, :file, :tabsize, :shortcut, :default_tag
34
19
  use Slim::Embedded, :enable_engines, :disable_engines, :pretty
35
20
  use Slim::Interpolation
21
+ use Slim::Splat::Filter, :merge_attrs, :attr_quote, :sort_attrs, :default_tag, :hyphen_attrs
22
+ use Slim::DoInserter
36
23
  use Slim::EndInserter
37
24
  use Slim::Controls, :disable_capture
38
- use Slim::SplatAttributes, :merge_attrs, :attr_quote, :sort_attrs, :default_tag
39
25
  html :AttributeSorter, :sort_attrs
40
26
  html :AttributeMerger, :merge_attrs
41
27
  use Slim::CodeAttributes, :merge_attrs
@@ -0,0 +1,14 @@
1
+ require 'slim'
2
+
3
+ module Slim
4
+ # Slim to ERB converter
5
+ #
6
+ # @example Conversion
7
+ # Slim::ERBConverter.new(options).call(slim_code) # outputs erb_code
8
+ #
9
+ # @api public
10
+ class ERBConverter < Engine
11
+ replace :Optimizer, Temple::Filters::CodeMerger
12
+ replace :Generator, Temple::Generators::ERB
13
+ end
14
+ end
data/lib/slim/filter.rb CHANGED
@@ -23,8 +23,8 @@ module Slim
23
23
  end
24
24
 
25
25
  # Pass-through handler
26
- def on_slim_output(code, escape, content)
27
- [:slim, :output, code, escape, compile(content)]
26
+ def on_slim_output(escape, code, content)
27
+ [:slim, :output, escape, code, compile(content)]
28
28
  end
29
29
  end
30
30
  end
@@ -45,6 +45,10 @@ module Slim
45
45
  yield if !value || (value.respond_to?(:empty?) && value.empty?)
46
46
  end
47
47
 
48
+ def to_s
49
+ scope.to_s
50
+ end
51
+
48
52
  private
49
53
 
50
54
  class Scope
@@ -89,6 +93,10 @@ module Slim
89
93
  @parent[name] if @parent
90
94
  end
91
95
 
96
+ def to_s
97
+ @dict.to_s
98
+ end
99
+
92
100
  private
93
101
 
94
102
  def has_key?(name)
@@ -12,17 +12,11 @@ module Slim
12
12
 
13
13
  def initialize(opts = {})
14
14
  super
15
- access = options[:dictionary_access]
16
- if access == :wrapped
17
- warn 'Slim::LogicLess - Wrapped dictionary access is deprecated and unsupported in Slim 2.0'
18
- access = DEFAULT_ACCESS_ORDER
19
- else
20
- access = [access].flatten.compact
21
- access.each do |type|
22
- raise ArgumentError, "Invalid dictionary access #{type.inspect}" unless DEFAULT_ACCESS_ORDER.include?(type)
23
- end
24
- raise ArgumentError, 'Option dictionary access is missing' if access.empty?
15
+ access = [options[:dictionary_access]].flatten.compact
16
+ access.each do |type|
17
+ raise ArgumentError, "Invalid dictionary access #{type.inspect}" unless DEFAULT_ACCESS_ORDER.include?(type)
25
18
  end
19
+ raise ArgumentError, 'Option dictionary access is missing' if access.empty?
26
20
  @access = access.inspect
27
21
  end
28
22
 
@@ -73,7 +67,14 @@ module Slim
73
67
  private
74
68
 
75
69
  def access(name)
76
- name == 'yield' ? name : "#{@context}[#{name.to_sym.inspect}]"
70
+ case name
71
+ when 'yield'
72
+ 'yield'
73
+ when 'self'
74
+ "#{@context}.to_s"
75
+ else
76
+ "#{@context}[#{name.to_sym.inspect}]"
77
+ end
77
78
  end
78
79
  end
79
80
  end
data/lib/slim/parser.rb CHANGED
@@ -1,12 +1,11 @@
1
+ # coding: utf-8
1
2
  module Slim
2
3
  # Parses Slim code and transforms it to a Temple expression
3
4
  # @api private
4
5
  class Parser < Temple::Parser
5
6
  define_options :file,
6
7
  :default_tag,
7
- :escape_quoted_attrs,
8
8
  :tabsize => 4,
9
- :encoding => 'utf-8',
10
9
  :shortcut => {
11
10
  '#' => { :attr => 'id' },
12
11
  '.' => { :attr => 'class' }
@@ -46,16 +45,15 @@ module Slim
46
45
  end
47
46
  @tag_shortcut, @attr_shortcut = {}, {}
48
47
  options[:shortcut].each do |k,v|
49
- v = deprecated_shortcut(v) if String === v
50
48
  raise ArgumentError, 'Shortcut requires :tag and/or :attr' unless (v[:attr] || v[:tag]) && (v.keys - [:attr, :tag]).empty?
51
49
  @tag_shortcut[k] = v[:tag] || options[:default_tag]
52
50
  if v.include?(:attr)
53
51
  @attr_shortcut[k] = v[:attr]
54
- raise ArgumentError, 'You can only use special characters for attribute shortcuts' if k =~ /[\w-]/
52
+ raise ArgumentError, 'You can only use special characters for attribute shortcuts' if k =~ /(#{WORD_RE}|-)/
55
53
  end
56
54
  end
57
- @attr_shortcut_regex = /\A(#{Regexp.union @attr_shortcut.keys})(\w[\w-]*\w|\w+)/
58
- @tag_regex = /\A(?:#{Regexp.union @tag_shortcut.keys}|\*(?=[^\s]+)|(\w[\w:-]*\w|\w+))/
55
+ @attr_shortcut_re = /\A(#{Regexp.union @attr_shortcut.keys})(#{WORD_RE}(?:#{WORD_RE}|-)*#{WORD_RE}|#{WORD_RE}+)/
56
+ @tag_re = /\A(?:#{Regexp.union @tag_shortcut.keys}|\*(?=[^\s]+)|(#{WORD_RE}(?:#{WORD_RE}|:|-)*#{WORD_RE}|#{WORD_RE}+))/
59
57
  end
60
58
 
61
59
  # Compile string to Temple expression
@@ -63,8 +61,6 @@ module Slim
63
61
  # @param [String] str Slim code
64
62
  # @return [Array] Temple expression representing the code]]
65
63
  def call(str)
66
- str = remove_bom(set_encoding(str))
67
-
68
64
  result = [:multi]
69
65
  reset(str.split(/\r?\n/), [result])
70
66
 
@@ -76,57 +72,18 @@ module Slim
76
72
 
77
73
  protected
78
74
 
79
- DELIMITERS = {
75
+ DELIMS = {
80
76
  '(' => ')',
81
77
  '[' => ']',
82
78
  '{' => '}',
83
79
  }.freeze
84
80
 
85
- ATTR_DELIM_REGEX = /\A\s*[#{Regexp.escape DELIMITERS.keys.join}]/
86
- DELIMITER_REGEX = /\A[#{Regexp.escape DELIMITERS.keys.join}]/
87
- ATTR_NAME = '\A\s*(\w[:\w-]*)'
88
- QUOTED_ATTR_REGEX = /#{ATTR_NAME}=(=?)("|')/
89
- CODE_ATTR_REGEX = /#{ATTR_NAME}=(=?)/
90
- QUOTED_ATTR_REGEX_20 = /#{ATTR_NAME}\s*=(=?)\s*("|')/
91
- CODE_ATTR_REGEX_20 = /#{ATTR_NAME}\s*=(=?)\s*/
92
-
93
- # Convert deprecated string shortcut to hash
94
- def deprecated_shortcut(v)
95
- warn "Slim :shortcut string values are deprecated and unsupported in Slim 2.0, use hash like { '#' => { :tag => 'div', :attr => 'id' } }"
96
- if v =~ /\A([^\s]+)\s+([^\s]+)\Z/
97
- { :tag => $1, :attr => $2 }
98
- else
99
- { :attr => v }
100
- end
101
- end
102
-
103
- # Set string encoding if option is set
104
- def set_encoding(s)
105
- if options[:encoding] && s.respond_to?(:encoding)
106
- old_enc = s.encoding
107
- s = s.dup if s.frozen?
108
- s.force_encoding(options[:encoding])
109
- # Fall back to old encoding if new encoding is invalid
110
- unless s.valid_encoding?
111
- s.force_encoding(old_enc)
112
- s.force_encoding(Encoding::BINARY) unless s.valid_encoding?
113
- end
114
- end
115
- s
116
- end
117
-
118
- # Remove unicode byte order mark from string
119
- def remove_bom(s)
120
- if s.respond_to?(:encoding)
121
- if s.encoding.name =~ /^UTF-(8|16|32)(BE|LE)?/
122
- s.gsub(Regexp.new("\\A\uFEFF".encode(s.encoding.name)), '')
123
- else
124
- s
125
- end
126
- else
127
- s.gsub(/\A\xEF\xBB\xBF/, '')
128
- end
129
- end
81
+ WORD_RE = ''.respond_to?(:encoding) ? '\p{Word}' : '\w'
82
+ DELIM_RE = /\A[#{Regexp.escape DELIMS.keys.join}]/
83
+ ATTR_DELIM_RE = /\A\s*([#{Regexp.escape DELIMS.keys.join}])/
84
+ ATTR_NAME = "\\A\\s*(#{WORD_RE}(?:#{WORD_RE}|:|-)*)"
85
+ QUOTED_ATTR_RE = /#{ATTR_NAME}\s*=(=?)\s*("|')/
86
+ CODE_ATTR_RE = /#{ATTR_NAME}\s*=(=?)\s*/
130
87
 
131
88
  def reset(lines = nil, stacks = nil)
132
89
  # Since you can indent however you like in Slim, we need to keep a list
@@ -237,7 +194,6 @@ module Slim
237
194
  @stacks.last << [:static, ' '] if trailing_ws
238
195
  when /\A</
239
196
  # Inline html
240
- # @stacks.last << parse_text_block(@line, @indents.last + 1)
241
197
  block = [:multi]
242
198
  @stacks.last << [:multi, [:slim, :interpolate, @line], block]
243
199
  @stacks << block
@@ -248,14 +204,15 @@ module Slim
248
204
  block = [:multi]
249
205
  @stacks.last << [:slim, :control, parse_broken_line, block]
250
206
  @stacks << block
251
- when /\A=/
207
+ when /\A=(=?)(['<>]*)/
252
208
  # Found an output block.
253
209
  # We expect the line to be broken or the next line to be indented.
254
- @line =~ /\A=(=?)('?)/
255
210
  @line = $'
211
+ trailing_ws = $2.include?('\'') || $2.include?('>')
256
212
  block = [:multi]
213
+ @stacks.last << [:static, ' '] if $2.include?('<')
257
214
  @stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block]
258
- @stacks.last << [:static, ' '] unless $2.empty?
215
+ @stacks.last << [:static, ' '] if trailing_ws
259
216
  @stacks << block
260
217
  when /\A(\w+):\s*\Z/
261
218
  # Embedded template detected. It is treated as block.
@@ -263,7 +220,7 @@ module Slim
263
220
  when /\Adoctype\s+/i
264
221
  # Found doctype declaration
265
222
  @stacks.last << [:html, :doctype, $'.strip]
266
- when @tag_regex
223
+ when @tag_re
267
224
  # Found a HTML tag.
268
225
  @line = $' if $1
269
226
  parse_tag($&)
@@ -340,14 +297,33 @@ module Slim
340
297
  tag = @tag_shortcut[tag]
341
298
  end
342
299
 
343
- tag = [:html, :tag, tag, parse_attributes]
300
+ # Find any shortcut attributes
301
+ attributes = [:html, :attrs]
302
+ while @line =~ @attr_shortcut_re
303
+ # The class/id attribute is :static instead of :slim :interpolate,
304
+ # because we don't want text interpolation in .class or #id shortcut
305
+ attributes << [:html, :attr, @attr_shortcut[$1], [:static, $2]]
306
+ @line = $'
307
+ end
308
+
309
+ @line =~ /\A[<>']*/
310
+ @line = $'
311
+ trailing_ws = $&.include?('\'') || $&.include?('>')
312
+ leading_ws = $&.include?('<')
313
+
314
+ parse_attributes(attributes)
315
+
316
+ tag = [:html, :tag, tag, attributes]
317
+
318
+ @stacks.last << [:static, ' '] if leading_ws
344
319
  @stacks.last << tag
320
+ @stacks.last << [:static, ' '] if trailing_ws
345
321
 
346
322
  case @line
347
323
  when /\A\s*:\s*/
348
324
  # Block expansion
349
325
  @line = $'
350
- (@line =~ @tag_regex) || syntax_error!('Expected tag')
326
+ (@line =~ @tag_re) || syntax_error!('Expected tag')
351
327
  @line = $' if $1
352
328
  content = [:multi]
353
329
  tag << content
@@ -355,12 +331,14 @@ module Slim
355
331
  @stacks << content
356
332
  parse_tag($&)
357
333
  @stacks.delete_at(i)
358
- when /\A\s*=(=?)('?)/
334
+ when /\A\s*=(=?)(['<>]*)/
359
335
  # Handle output code
360
336
  @line = $'
337
+ trailing_ws2 = $2.include?('\'') || $2.include?('>')
361
338
  block = [:multi]
339
+ @stacks.last << [:static, ' '] if !leading_ws && $2.include?('<')
362
340
  tag << [:slim, :output, $1 != '=', parse_broken_line, block]
363
- @stacks.last << [:static, ' '] unless $2.empty?
341
+ @stacks.last << [:static, ' '] if !trailing_ws && trailing_ws2
364
342
  @stacks << block
365
343
  when /\A\s*\/\s*/
366
344
  # Closed tag. Do nothing
@@ -377,31 +355,17 @@ module Slim
377
355
  end
378
356
  end
379
357
 
380
- def parse_attributes
381
- attributes = [:html, :attrs]
382
-
383
- # Find any shortcut attributes
384
- while @line =~ @attr_shortcut_regex
385
- # The class/id attribute is :static instead of :slim :interpolate,
386
- # because we don't want text interpolation in .class or #id shortcut
387
- attributes << [:html, :attr, @attr_shortcut[$1], [:static, $2]]
388
- @line = $'
389
- end
390
-
358
+ def parse_attributes(attributes)
391
359
  # Check to see if there is a delimiter right after the tag name
392
360
  delimiter = nil
393
- if @line =~ ATTR_DELIM_REGEX
394
- if $&.size > 1
395
- warn "#{options[:file]}:#{@lineno} - spaces around attribute delimiters will be allowed in Slim 2.0. Your code is incompatible."
396
- else
397
- delimiter = DELIMITERS[$&]
398
- @line.slice!(0)
399
- end
361
+ if @line =~ ATTR_DELIM_RE
362
+ delimiter = DELIMS[$1]
363
+ @line = $'
400
364
  end
401
365
 
402
366
  if delimiter
403
- boolean_attr_regex = /#{ATTR_NAME}(?=(\s|#{Regexp.escape delimiter}|\Z))/
404
- end_regex = /\A\s*#{Regexp.escape delimiter}/
367
+ boolean_attr_re = /#{ATTR_NAME}(?=(\s|#{Regexp.escape delimiter}|\Z))/
368
+ end_re = /\A\s*#{Regexp.escape delimiter}/
405
369
  end
406
370
 
407
371
  while true
@@ -410,46 +374,28 @@ module Slim
410
374
  # Splat attribute
411
375
  @line = $'
412
376
  attributes << [:slim, :splat, parse_ruby_code(delimiter)]
413
- when QUOTED_ATTR_REGEX
377
+ when QUOTED_ATTR_RE
414
378
  # Value is quoted (static)
415
379
  @line = $'
416
- name = $1
417
- escape = $2.empty?
418
- quote = $3
419
- value = parse_quoted_attribute(quote)
420
- if escape && !options[:escape_quoted_attrs] && value =~ /&(amp|quot|gt|lt);/
421
- warn "#{options[:file]}:#{@lineno} - quoted attribute value '#{value}' might be double escaped in Slim 2.0. Remove manually escaped entities and set :escape_quoted_attrs => true! :escaped_quoted_attrs is activated on Slim 2.0 by default."
422
- end
423
- attributes << [:html, :attr, name,
424
- [:escape, options[:escape_quoted_attrs] && escape,
425
- [:slim, :interpolate, value]]]
426
- when CODE_ATTR_REGEX
380
+ attributes << [:html, :attr, $1,
381
+ [:escape, $2.empty?, [:slim, :interpolate, parse_quoted_attribute($3)]]]
382
+ when CODE_ATTR_RE
427
383
  # Value is ruby code
428
384
  @line = $'
429
385
  name = $1
430
386
  escape = $2.empty?
431
387
  value = parse_ruby_code(delimiter)
432
- # Remove attribute wrapper which doesn't belong to the ruby code
433
- # e.g id=[hash[:a] + hash[:b]]
434
- if value =~ /\A[\[\{]/ && DELIMITERS[$&] == value[-1, 1]
435
- warn "#{options[:file]}:#{@lineno} - ruby attribute value #{value} with curly braces/brackets is deprecated and unsupported in Slim 2.0. Use parentheses!"
436
- value = value[1..-2]
437
- end
438
388
  syntax_error!('Invalid empty attribute') if value.empty?
439
389
  attributes << [:html, :attr, name, [:slim, :attrvalue, escape, value]]
440
390
  else
441
- if @line =~ QUOTED_ATTR_REGEX_20 || @line =~ CODE_ATTR_REGEX_20
442
- warn "#{options[:file]}:#{@lineno} - you have spaces around =, this will be interpreted as attribute in Slim 2.0."
443
- end
444
-
445
391
  break unless delimiter
446
392
 
447
393
  case @line
448
- when boolean_attr_regex
394
+ when boolean_attr_re
449
395
  # Boolean attribute
450
396
  @line = $'
451
397
  attributes << [:html, :attr, $1, [:slim, :attrvalue, false, 'true']]
452
- when end_regex
398
+ when end_re
453
399
  # Find ending delimiter
454
400
  @line = $'
455
401
  break
@@ -465,17 +411,15 @@ module Slim
465
411
  end
466
412
  end
467
413
  end
468
-
469
- attributes
470
414
  end
471
415
 
472
416
  def parse_ruby_code(outer_delimiter)
473
417
  code, count, delimiter, close_delimiter = '', 0, nil, nil
474
418
 
475
419
  # Attribute ends with space or attribute delimiter
476
- end_regex = /\A[\s#{Regexp.escape outer_delimiter.to_s}]/
420
+ end_re = /\A[\s#{Regexp.escape outer_delimiter.to_s}]/
477
421
 
478
- until @line.empty? || (count == 0 && @line =~ end_regex)
422
+ until @line.empty? || (count == 0 && @line =~ end_re)
479
423
  if @line =~ /\A[,\\]\Z/
480
424
  code << @line << "\n"
481
425
  expect_next_line
@@ -486,9 +430,9 @@ module Slim
486
430
  elsif @line[0] == close_delimiter[0]
487
431
  count -= 1
488
432
  end
489
- elsif @line =~ DELIMITER_REGEX
433
+ elsif @line =~ DELIM_RE
490
434
  count = 1
491
- delimiter, close_delimiter = $&, DELIMITERS[$&]
435
+ delimiter, close_delimiter = $&, DELIMS[$&]
492
436
  end
493
437
  code << @line.slice!(0)
494
438
  end