actionview 7.1.6 → 7.2.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -425
  3. data/README.rdoc +1 -1
  4. data/lib/action_view/base.rb +24 -9
  5. data/lib/action_view/cache_expiry.rb +9 -3
  6. data/lib/action_view/dependency_tracker/{ripper_tracker.rb → ruby_tracker.rb} +4 -3
  7. data/lib/action_view/dependency_tracker.rb +1 -1
  8. data/lib/action_view/digestor.rb +6 -2
  9. data/lib/action_view/gem_version.rb +2 -2
  10. data/lib/action_view/helpers/asset_tag_helper.rb +19 -7
  11. data/lib/action_view/helpers/atom_feed_helper.rb +1 -1
  12. data/lib/action_view/helpers/cache_helper.rb +2 -2
  13. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  14. data/lib/action_view/helpers/form_helper.rb +222 -217
  15. data/lib/action_view/helpers/form_options_helper.rb +6 -3
  16. data/lib/action_view/helpers/form_tag_helper.rb +80 -47
  17. data/lib/action_view/helpers/output_safety_helper.rb +5 -6
  18. data/lib/action_view/helpers/tag_helper.rb +208 -18
  19. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  20. data/lib/action_view/helpers/text_helper.rb +11 -4
  21. data/lib/action_view/helpers/url_helper.rb +3 -77
  22. data/lib/action_view/layouts.rb +8 -10
  23. data/lib/action_view/log_subscriber.rb +8 -4
  24. data/lib/action_view/railtie.rb +0 -1
  25. data/lib/action_view/render_parser/prism_render_parser.rb +127 -0
  26. data/lib/action_view/{ripper_ast_parser.rb → render_parser/ripper_render_parser.rb} +152 -9
  27. data/lib/action_view/render_parser.rb +21 -169
  28. data/lib/action_view/renderer/abstract_renderer.rb +1 -1
  29. data/lib/action_view/renderer/partial_renderer.rb +2 -2
  30. data/lib/action_view/renderer/renderer.rb +32 -38
  31. data/lib/action_view/renderer/template_renderer.rb +3 -3
  32. data/lib/action_view/rendering.rb +4 -4
  33. data/lib/action_view/template/error.rb +11 -0
  34. data/lib/action_view/template/handlers/erb.rb +45 -37
  35. data/lib/action_view/template/renderable.rb +7 -1
  36. data/lib/action_view/template/resolver.rb +0 -2
  37. data/lib/action_view/template.rb +36 -8
  38. data/lib/action_view/test_case.rb +7 -10
  39. data/lib/action_view.rb +1 -0
  40. metadata +14 -13
@@ -195,42 +195,6 @@ module ActionView
195
195
  # link_to "Visit Other Site", "https://rubyonrails.org/", data: { turbo_confirm: "Are you sure?" }
196
196
  # # => <a href="https://rubyonrails.org/" data-turbo-confirm="Are you sure?">Visit Other Site</a>
197
197
  #
198
- # ==== Deprecated: \Rails UJS Attributes
199
- #
200
- # Prior to \Rails 7, \Rails shipped with a JavaScript library called <tt>@rails/ujs</tt> on by default. Following \Rails 7,
201
- # this library is no longer on by default. This library integrated with the following options:
202
- #
203
- # * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically
204
- # create an HTML form and immediately submit the form for processing using
205
- # the HTTP verb specified. Useful for having links perform a POST operation
206
- # in dangerous actions like deleting a record (which search bots can follow
207
- # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
208
- # Note that if the user has JavaScript disabled, the request will fall back
209
- # to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
210
- # disabled clicking the link will have no effect. If you are relying on the
211
- # POST behavior, you should check for it in your controller's action by using
212
- # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>patch?</tt>, or <tt>put?</tt>.
213
- # * <tt>remote: true</tt> - This will allow <tt>@rails/ujs</tt>
214
- # to make an Ajax request to the URL in question instead of following
215
- # the link.
216
- #
217
- # <tt>@rails/ujs</tt> also integrated with the following +:data+ options:
218
- #
219
- # * <tt>confirm: "question?"</tt> - This will allow <tt>@rails/ujs</tt>
220
- # to prompt with the question specified (in this case, the
221
- # resulting text would be <tt>question?</tt>). If the user accepts, the
222
- # link is processed normally, otherwise no action is taken.
223
- # * <tt>:disable_with</tt> - Value of this parameter will be used as the
224
- # name for a disabled version of the link.
225
- #
226
- # ===== \Rails UJS Examples
227
- #
228
- # link_to "Remove Profile", profile_path(@profile), method: :delete
229
- # # => <a href="/profiles/1" rel="nofollow" data-method="delete">Remove Profile</a>
230
- #
231
- # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
232
- # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
233
- #
234
198
  def link_to(name = nil, options = nil, html_options = nil, &block)
235
199
  html_options, options, name = options, name, block if block_given?
236
200
  options ||= {}
@@ -256,8 +220,9 @@ module ActionView
256
220
  # +:form_class+ option within +html_options+. It defaults to
257
221
  # <tt>"button_to"</tt> to allow styling of the form and its children.
258
222
  #
259
- # The form submits a POST request by default. You can specify a different
260
- # HTTP verb via the +:method+ option within +html_options+.
223
+ # The form submits a POST request by default if the object is not persisted;
224
+ # conversely, if the object is persisted, it will submit a PATCH request.
225
+ # To specify a different HTTP verb use the +:method+ option within +html_options+.
261
226
  #
262
227
  # If the HTML button generated from +button_to+ does not work with your layout, you can
263
228
  # consider using the +link_to+ method with the +data-turbo-method+
@@ -328,32 +293,6 @@ module ActionView
328
293
  # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6" autocomplete="off"/>
329
294
  # # </form>"
330
295
  #
331
- # ==== Deprecated: \Rails UJS Attributes
332
- #
333
- # Prior to \Rails 7, \Rails shipped with a JavaScript library called <tt>@rails/ujs</tt> on by default. Following \Rails 7,
334
- # this library is no longer on by default. This library integrated with the following options:
335
- #
336
- # * <tt>:remote</tt> - If set to true, will allow <tt>@rails/ujs</tt> to control the
337
- # submit behavior. By default this behavior is an Ajax submit.
338
- #
339
- # <tt>@rails/ujs</tt> also integrated with the following +:data+ options:
340
- #
341
- # * <tt>confirm: "question?"</tt> - This will allow <tt>@rails/ujs</tt>
342
- # to prompt with the question specified (in this case, the
343
- # resulting text would be <tt>question?</tt>). If the user accepts, the
344
- # button is processed normally, otherwise no action is taken.
345
- # * <tt>:disable_with</tt> - Value of this parameter will be
346
- # used as the value for a disabled version of the submit
347
- # button when the form is submitted.
348
- #
349
- # ===== \Rails UJS Examples
350
- #
351
- # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
352
- # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
353
- # # <button type="submit">Create</button>
354
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6" autocomplete="off"/>
355
- # # </form>"
356
- #
357
296
  def button_to(name = nil, options = nil, html_options = nil, &block)
358
297
  html_options, options = options, name if block_given?
359
298
  html_options ||= {}
@@ -638,19 +577,6 @@ module ActionView
638
577
  url_string == request_uri
639
578
  end
640
579
 
641
- if RUBY_VERSION.start_with?("2.7")
642
- using Module.new {
643
- refine UrlHelper do
644
- alias :_current_page? :current_page?
645
- end
646
- }
647
-
648
- def current_page?(*args) # :nodoc:
649
- options = args.pop
650
- options.is_a?(Hash) ? _current_page?(*args, **options) : _current_page?(*args, options)
651
- end
652
- end
653
-
654
580
  # Creates an SMS anchor link tag to the specified +phone_number+. When the
655
581
  # link is clicked, the default SMS messaging app is opened ready to send a
656
582
  # message to the linked phone number. If the +body+ option is specified,
@@ -209,11 +209,9 @@ module ActionView
209
209
 
210
210
  included do
211
211
  class_attribute :_layout, instance_accessor: false
212
- class_attribute :_layout_conditions, instance_accessor: false, default: {}
212
+ class_attribute :_layout_conditions, instance_accessor: false, instance_reader: true, default: {}
213
213
 
214
214
  _write_layout_method
215
-
216
- delegate :_layout_conditions, to: :class
217
215
  end
218
216
 
219
217
  module ClassMethods
@@ -286,7 +284,7 @@ module ActionView
286
284
  silence_redefinition_of_method(:_layout)
287
285
 
288
286
  prefixes = /\blayouts/.match?(_implied_layout_name) ? [] : ["layouts"]
289
- default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, [], { formats: formats }).first || super"
287
+ default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}, false, keys, { formats: formats }).first || super"
290
288
  name_clause = if name
291
289
  default_behavior
292
290
  else
@@ -327,7 +325,7 @@ module ActionView
327
325
 
328
326
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
329
327
  # frozen_string_literal: true
330
- def _layout(lookup_context, formats)
328
+ def _layout(lookup_context, formats, keys)
331
329
  if _conditional_layout?
332
330
  #{layout_definition}
333
331
  else
@@ -391,8 +389,8 @@ module ActionView
391
389
  case name
392
390
  when String then _normalize_layout(name)
393
391
  when Proc then name
394
- when true then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, true) }
395
- when :default then Proc.new { |lookup_context, formats| _default_layout(lookup_context, formats, false) }
392
+ when true then Proc.new { |lookup_context, formats, keys| _default_layout(lookup_context, formats, keys, true) }
393
+ when :default then Proc.new { |lookup_context, formats, keys| _default_layout(lookup_context, formats, keys, false) }
396
394
  when false, nil then nil
397
395
  else
398
396
  raise ArgumentError,
@@ -414,9 +412,9 @@ module ActionView
414
412
  #
415
413
  # ==== Returns
416
414
  # * <tt>template</tt> - The template object for the default layout (or +nil+)
417
- def _default_layout(lookup_context, formats, require_layout = false)
415
+ def _default_layout(lookup_context, formats, keys, require_layout = false)
418
416
  begin
419
- value = _layout(lookup_context, formats) if action_has_layout?
417
+ value = _layout(lookup_context, formats, keys) if action_has_layout?
420
418
  rescue NameError => e
421
419
  raise e, "Could not render layout: #{e.message}"
422
420
  end
@@ -430,7 +428,7 @@ module ActionView
430
428
  end
431
429
 
432
430
  def _include_layout?(options)
433
- (options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
431
+ !options.keys.intersect?([:body, :plain, :html, :inline, :partial]) || options.key?(:layout)
434
432
  end
435
433
  end
436
434
  end
@@ -18,7 +18,7 @@ module ActionView
18
18
  info do
19
19
  message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
20
20
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
21
- message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
21
+ message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
22
22
  end
23
23
  end
24
24
  subscribe_log_level :render_template, :debug
@@ -27,7 +27,7 @@ module ActionView
27
27
  debug do
28
28
  message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
29
29
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
30
- message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
30
+ message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
31
31
  message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
32
32
  message
33
33
  end
@@ -37,7 +37,7 @@ module ActionView
37
37
  def render_layout(event)
38
38
  info do
39
39
  message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
40
- message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
40
+ message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
41
41
  end
42
42
  end
43
43
  subscribe_log_level :render_layout, :info
@@ -48,7 +48,7 @@ module ActionView
48
48
  debug do
49
49
  message = +" Rendered collection of #{from_rails_root(identifier)}"
50
50
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
51
- message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
51
+ message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
52
52
  message
53
53
  end
54
54
  end
@@ -96,6 +96,10 @@ module ActionView
96
96
 
97
97
  def finish(name, id, payload)
98
98
  end
99
+
100
+ def silenced?(_)
101
+ logger.nil? || !logger.debug?
102
+ end
99
103
  end
100
104
 
101
105
  def self.attach_to(*)
@@ -116,7 +116,6 @@ module ActionView
116
116
  view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
117
117
 
118
118
  app.reloaders << view_reloader
119
- view_reloader.execute
120
119
  app.reloader.to_run do
121
120
  require_unload_lock!
122
121
  view_reloader.execute
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module RenderParser
5
+ class PrismRenderParser < Base # :nodoc:
6
+ def render_calls
7
+ queue = [Prism.parse(@code).value]
8
+ templates = []
9
+
10
+ while (node = queue.shift)
11
+ queue.concat(node.compact_child_nodes)
12
+ next unless node.is_a?(Prism::CallNode)
13
+
14
+ options = render_call_options(node)
15
+ next unless options
16
+
17
+ render_type = (options.keys & RENDER_TYPE_KEYS)[0]
18
+ template, object_template = render_call_template(options[render_type])
19
+ next unless template
20
+
21
+ if options.key?(:object) || options.key?(:collection) || object_template
22
+ next if options.key?(:object) && options.key?(:collection)
23
+ next unless options.key?(:partial)
24
+ end
25
+
26
+ if options[:spacer_template].is_a?(Prism::StringNode)
27
+ templates << partial_to_virtual_path(:partial, options[:spacer_template].unescaped)
28
+ end
29
+
30
+ templates << partial_to_virtual_path(render_type, template)
31
+
32
+ if render_type != :layout && options[:layout].is_a?(Prism::StringNode)
33
+ templates << partial_to_virtual_path(:layout, options[:layout].unescaped)
34
+ end
35
+ end
36
+
37
+ templates
38
+ end
39
+
40
+ private
41
+ # Accept a call node and return a hash of options for the render call.
42
+ # If it doesn't match the expected format, return nil.
43
+ def render_call_options(node)
44
+ # We are only looking for calls to render or render_to_string.
45
+ name = node.name.to_sym
46
+ return if name != :render && name != :render_to_string
47
+
48
+ # We are only looking for calls with arguments.
49
+ arguments = node.arguments
50
+ return unless arguments
51
+
52
+ arguments = arguments.arguments
53
+ length = arguments.length
54
+
55
+ # Get rid of any parentheses to get directly to the contents.
56
+ arguments.map! do |argument|
57
+ current = argument
58
+
59
+ while current.is_a?(Prism::ParenthesesNode) &&
60
+ current.body.is_a?(Prism::StatementsNode) &&
61
+ current.body.body.length == 1
62
+ current = current.body.body.first
63
+ end
64
+
65
+ current
66
+ end
67
+
68
+ # We are only looking for arguments that are either a string with an
69
+ # array of locals or a keyword hash with symbol keys.
70
+ options =
71
+ if (length == 1 || length == 2) && !arguments[0].is_a?(Prism::KeywordHashNode)
72
+ { partial: arguments[0], locals: arguments[1] }
73
+ elsif length == 1 &&
74
+ arguments[0].is_a?(Prism::KeywordHashNode) &&
75
+ arguments[0].elements.all? do |element|
76
+ element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)
77
+ end
78
+ arguments[0].elements.to_h do |element|
79
+ [element.key.unescaped.to_sym, element.value]
80
+ end
81
+ end
82
+
83
+ return unless options
84
+
85
+ # Here we validate that the options have the keys we expect.
86
+ keys = options.keys
87
+ return if !keys.intersect?(RENDER_TYPE_KEYS)
88
+ return if (keys - ALL_KNOWN_KEYS).any?
89
+
90
+ # Finally, we can return a valid set of options.
91
+ options
92
+ end
93
+
94
+ # Accept the node that is being passed in the position of the template
95
+ # and return the template name and whether or not it is an object
96
+ # template.
97
+ def render_call_template(node)
98
+ object_template = false
99
+ template =
100
+ if node.is_a?(Prism::StringNode)
101
+ path = node.unescaped
102
+ path.include?("/") ? path : "#{directory}/#{path}"
103
+ else
104
+ dependency =
105
+ case node.type
106
+ when :class_variable_read_node
107
+ node.slice[2..]
108
+ when :instance_variable_read_node
109
+ node.slice[1..]
110
+ when :global_variable_read_node
111
+ node.slice[1..]
112
+ when :local_variable_read_node
113
+ node.slice
114
+ when :call_node
115
+ node.name.to_s
116
+ else
117
+ return
118
+ end
119
+
120
+ "#{dependency.pluralize}/#{dependency.singularize}"
121
+ end
122
+
123
+ [template, object_template]
124
+ end
125
+ end
126
+ end
127
+ end
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "ripper"
4
-
5
3
  module ActionView
6
- class RenderParser
7
- module RipperASTParser # :nodoc:
4
+ module RenderParser
5
+ class RipperRenderParser < Base # :nodoc:
8
6
  class Node < ::Array # :nodoc:
9
7
  attr_reader :type
10
8
 
@@ -183,16 +181,161 @@ module ActionView
183
181
  end
184
182
  end
185
183
 
186
- extend self
187
-
188
- def parse_render_nodes(code)
189
- parser = RenderCallExtractor.new(code)
184
+ def render_calls
185
+ parser = RenderCallExtractor.new(@code)
190
186
  parser.parse
191
187
 
192
188
  parser.render_calls.group_by(&:first).to_h do |method, nodes|
193
189
  [ method.to_sym, nodes.collect { |v| v[1] } ]
194
- end
190
+ end.map do |method, nodes|
191
+ nodes.map { |n| parse_render(n) }
192
+ end.flatten.compact
195
193
  end
194
+
195
+ private
196
+ def resolve_path_directory(path)
197
+ if path.include?("/")
198
+ path
199
+ else
200
+ "#{directory}/#{path}"
201
+ end
202
+ end
203
+
204
+ # Convert
205
+ # render("foo", ...)
206
+ # into either
207
+ # render(template: "foo", ...)
208
+ # or
209
+ # render(partial: "foo", ...)
210
+ def normalize_args(string, options_hash)
211
+ if options_hash
212
+ { partial: string, locals: options_hash }
213
+ else
214
+ { partial: string }
215
+ end
216
+ end
217
+
218
+ def parse_render(node)
219
+ node = node.argument_nodes
220
+
221
+ if (node.length == 1 || node.length == 2) && !node[0].hash?
222
+ if node.length == 1
223
+ options = normalize_args(node[0], nil)
224
+ elsif node.length == 2
225
+ options = normalize_args(node[0], node[1])
226
+ end
227
+
228
+ return nil unless options
229
+
230
+ parse_render_from_options(options)
231
+ elsif node.length == 1 && node[0].hash?
232
+ options = parse_hash_to_symbols(node[0])
233
+
234
+ return nil unless options
235
+
236
+ parse_render_from_options(options)
237
+ else
238
+ nil
239
+ end
240
+ end
241
+
242
+ def parse_hash(node)
243
+ node.hash? && node.to_hash
244
+ end
245
+
246
+ def parse_hash_to_symbols(node)
247
+ hash = parse_hash(node)
248
+
249
+ return unless hash
250
+
251
+ hash.transform_keys do |key_node|
252
+ key = parse_sym(key_node)
253
+
254
+ return unless key
255
+
256
+ key
257
+ end
258
+ end
259
+
260
+ def parse_render_from_options(options_hash)
261
+ renders = []
262
+ keys = options_hash.keys
263
+
264
+ if (keys & RENDER_TYPE_KEYS).size < 1
265
+ # Must have at least one of render keys
266
+ return nil
267
+ end
268
+
269
+ if (keys - ALL_KNOWN_KEYS).any?
270
+ # de-opt in case of unknown option
271
+ return nil
272
+ end
273
+
274
+ render_type = (keys & RENDER_TYPE_KEYS)[0]
275
+
276
+ node = options_hash[render_type]
277
+
278
+ if node.string?
279
+ template = resolve_path_directory(node.to_string)
280
+ else
281
+ if node.variable_reference?
282
+ dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
283
+ elsif node.vcall?
284
+ dependency = node.variable_name
285
+ elsif node.call?
286
+ dependency = node.call_method_name
287
+ else
288
+ return
289
+ end
290
+
291
+ object_template = true
292
+ template = "#{dependency.pluralize}/#{dependency.singularize}"
293
+ end
294
+
295
+ return unless template
296
+
297
+ if spacer_template = render_template_with_spacer?(options_hash)
298
+ virtual_path = partial_to_virtual_path(:partial, spacer_template)
299
+ renders << virtual_path
300
+ end
301
+
302
+ if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
303
+ return nil if options_hash.key?(:object) && options_hash.key?(:collection)
304
+ return nil unless options_hash.key?(:partial)
305
+ end
306
+
307
+ virtual_path = partial_to_virtual_path(render_type, template)
308
+ renders << virtual_path
309
+
310
+ # Support for rendering multiple templates (i.e. a partial with a layout)
311
+ if layout_template = render_template_with_layout?(render_type, options_hash)
312
+ virtual_path = partial_to_virtual_path(:layout, layout_template)
313
+
314
+ renders << virtual_path
315
+ end
316
+
317
+ renders
318
+ end
319
+
320
+ def parse_str(node)
321
+ node.string? && node.to_string
322
+ end
323
+
324
+ def parse_sym(node)
325
+ node.symbol? && node.to_symbol
326
+ end
327
+
328
+ def render_template_with_layout?(render_type, options_hash)
329
+ if render_type != :layout && options_hash.key?(:layout)
330
+ parse_str(options_hash[:layout])
331
+ end
332
+ end
333
+
334
+ def render_template_with_spacer?(options_hash)
335
+ if options_hash.key?(:spacer_template)
336
+ parse_str(options_hash[:spacer_template])
337
+ end
338
+ end
196
339
  end
197
340
  end
198
341
  end