actionview 8.0.3 → 8.1.0.rc1

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -67
  3. data/lib/action_view/base.rb +5 -2
  4. data/lib/action_view/buffers.rb +1 -1
  5. data/lib/action_view/dependency_tracker/erb_tracker.rb +1 -1
  6. data/lib/action_view/dependency_tracker.rb +6 -1
  7. data/lib/action_view/gem_version.rb +3 -3
  8. data/lib/action_view/helpers/asset_tag_helper.rb +23 -4
  9. data/lib/action_view/helpers/capture_helper.rb +2 -2
  10. data/lib/action_view/helpers/controller_helper.rb +6 -2
  11. data/lib/action_view/helpers/date_helper.rb +17 -0
  12. data/lib/action_view/helpers/form_helper.rb +2 -3
  13. data/lib/action_view/helpers/form_options_helper.rb +10 -12
  14. data/lib/action_view/helpers/form_tag_helper.rb +17 -10
  15. data/lib/action_view/helpers/javascript_helper.rb +5 -1
  16. data/lib/action_view/helpers/number_helper.rb +14 -0
  17. data/lib/action_view/helpers/tag_helper.rb +31 -34
  18. data/lib/action_view/helpers/tags/base.rb +2 -0
  19. data/lib/action_view/helpers/tags/check_box.rb +7 -1
  20. data/lib/action_view/helpers/tags/datetime_field.rb +1 -1
  21. data/lib/action_view/helpers/tags/file_field.rb +3 -1
  22. data/lib/action_view/helpers/tags/hidden_field.rb +1 -1
  23. data/lib/action_view/helpers/tags/select.rb +6 -1
  24. data/lib/action_view/helpers/tags/select_renderer.rb +5 -3
  25. data/lib/action_view/helpers/translation_helper.rb +6 -1
  26. data/lib/action_view/helpers/url_helper.rb +37 -9
  27. data/lib/action_view/locale/en.yml +3 -0
  28. data/lib/action_view/log_subscriber.rb +1 -4
  29. data/lib/action_view/railtie.rb +11 -0
  30. data/lib/action_view/record_identifier.rb +21 -0
  31. data/lib/action_view/renderer/partial_renderer.rb +16 -0
  32. data/lib/action_view/structured_event_subscriber.rb +97 -0
  33. data/lib/action_view/template/error.rb +7 -3
  34. data/lib/action_view/template/handlers/erb.rb +37 -12
  35. data/lib/action_view/test_case.rb +50 -52
  36. data/lib/action_view.rb +3 -0
  37. metadata +11 -10
@@ -26,6 +26,8 @@ module ActionView
26
26
 
27
27
  # Delegates to ActiveSupport::NumberHelper#number_to_phone.
28
28
  #
29
+ # number_to_phone("1234567890") # => "123-456-7890"
30
+ #
29
31
  # Additionally, supports a +:raise+ option that will cause
30
32
  # InvalidNumberError to be raised if +number+ is not a valid number:
31
33
  #
@@ -42,6 +44,8 @@ module ActionView
42
44
 
43
45
  # Delegates to ActiveSupport::NumberHelper#number_to_currency.
44
46
  #
47
+ # number_to_currency("1234") # => "$1234.00"
48
+ #
45
49
  # Additionally, supports a +:raise+ option that will cause
46
50
  # InvalidNumberError to be raised if +number+ is not a valid number:
47
51
  #
@@ -54,6 +58,8 @@ module ActionView
54
58
 
55
59
  # Delegates to ActiveSupport::NumberHelper#number_to_percentage.
56
60
  #
61
+ # number_to_percentage("99") # => "99.000%"
62
+ #
57
63
  # Additionally, supports a +:raise+ option that will cause
58
64
  # InvalidNumberError to be raised if +number+ is not a valid number:
59
65
  #
@@ -66,6 +72,8 @@ module ActionView
66
72
 
67
73
  # Delegates to ActiveSupport::NumberHelper#number_to_delimited.
68
74
  #
75
+ # number_with_delimiter("1234") # => "1,234"
76
+ #
69
77
  # Additionally, supports a +:raise+ option that will cause
70
78
  # InvalidNumberError to be raised if +number+ is not a valid number:
71
79
  #
@@ -78,6 +86,8 @@ module ActionView
78
86
 
79
87
  # Delegates to ActiveSupport::NumberHelper#number_to_rounded.
80
88
  #
89
+ # number_with_precision("1234") # => "1234.000"
90
+ #
81
91
  # Additionally, supports a +:raise+ option that will cause
82
92
  # InvalidNumberError to be raised if +number+ is not a valid number:
83
93
  #
@@ -90,6 +100,8 @@ module ActionView
90
100
 
91
101
  # Delegates to ActiveSupport::NumberHelper#number_to_human_size.
92
102
  #
103
+ # number_to_human_size("1234") # => "1.21 KB"
104
+ #
93
105
  # Additionally, supports a +:raise+ option that will cause
94
106
  # InvalidNumberError to be raised if +number+ is not a valid number:
95
107
  #
@@ -102,6 +114,8 @@ module ActionView
102
114
 
103
115
  # Delegates to ActiveSupport::NumberHelper#number_to_human.
104
116
  #
117
+ # number_to_human("1234") # => "1.23 Thousand"
118
+ #
105
119
  # Additionally, supports a +:raise+ option that will cause
106
120
  # InvalidNumberError to be raised if +number+ is not a valid number:
107
121
  #
@@ -44,9 +44,6 @@ module ActionView
44
44
  PRE_CONTENT_STRINGS["textarea"] = "\n"
45
45
 
46
46
  class TagBuilder # :nodoc:
47
- include CaptureHelper
48
- include OutputSafetyHelper
49
-
50
47
  def self.define_element(name, code_generator:, method_name: name)
51
48
  return if method_defined?(name)
52
49
 
@@ -226,17 +223,7 @@ module ActionView
226
223
  tag_options(attributes.to_h).to_s.strip.html_safe
227
224
  end
228
225
 
229
- def tag_string(name, content = nil, options, escape: true, &block)
230
- content = @view_context.capture(self, &block) if block
231
-
232
- content_tag_string(name, content, options, escape)
233
- end
234
-
235
- def self_closing_tag_string(name, options, escape = true, tag_suffix = " />")
236
- "<#{name}#{tag_options(options, escape)}#{tag_suffix}".html_safe
237
- end
238
-
239
- def content_tag_string(name, content, options, escape = true)
226
+ def content_tag_string(name, content, options, escape = true) # :nodoc:
240
227
  tag_options = tag_options(options, escape) if options
241
228
 
242
229
  if escape && content.present?
@@ -245,7 +232,7 @@ module ActionView
245
232
  "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe
246
233
  end
247
234
 
248
- def tag_options(options, escape = true)
235
+ def tag_options(options, escape = true) # :nodoc:
249
236
  return if options.blank?
250
237
  output = +""
251
238
  sep = " "
@@ -266,7 +253,7 @@ module ActionView
266
253
  tokens = TagHelper.build_tag_values(v)
267
254
  next if tokens.none?
268
255
 
269
- v = safe_join(tokens, " ")
256
+ v = @view_context.safe_join(tokens, " ")
270
257
  else
271
258
  v = v.to_s
272
259
  end
@@ -287,28 +274,38 @@ module ActionView
287
274
  output unless output.empty?
288
275
  end
289
276
 
290
- def boolean_tag_option(key)
291
- %(#{key}="#{key}")
292
- end
277
+ private
278
+ def tag_string(name, content = nil, options, escape: true, &block)
279
+ content = @view_context.capture(self, &block) if block
293
280
 
294
- def tag_option(key, value, escape)
295
- key = ERB::Util.xml_name_escape(key) if escape
296
-
297
- case value
298
- when Array, Hash
299
- value = TagHelper.build_tag_values(value) if key.to_s == "class"
300
- value = escape ? safe_join(value, " ") : value.join(" ")
301
- when Regexp
302
- value = escape ? ERB::Util.unwrapped_html_escape(value.source) : value.source
303
- else
304
- value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
281
+ content_tag_string(name, content, options, escape)
305
282
  end
306
- value = value.gsub('"', "&quot;") if value.include?('"')
307
283
 
308
- %(#{key}="#{value}")
309
- end
284
+ def self_closing_tag_string(name, options, escape = true, tag_suffix = " />")
285
+ "<#{name}#{tag_options(options, escape)}#{tag_suffix}".html_safe
286
+ end
287
+
288
+ def boolean_tag_option(key)
289
+ %(#{key}="#{key}")
290
+ end
291
+
292
+ def tag_option(key, value, escape)
293
+ key = ERB::Util.xml_name_escape(key) if escape
294
+
295
+ case value
296
+ when Array, Hash
297
+ value = TagHelper.build_tag_values(value) if key.to_s == "class"
298
+ value = escape ? @view_context.safe_join(value, " ") : value.join(" ")
299
+ when Regexp
300
+ value = escape ? ERB::Util.unwrapped_html_escape(value.source) : value.source
301
+ else
302
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s
303
+ end
304
+ value = value.gsub('"', "&quot;") if value.include?('"')
305
+
306
+ %(#{key}="#{value}")
307
+ end
310
308
 
311
- private
312
309
  def prefix_tag_option(prefix, key, value, escape)
313
310
  key = "#{prefix}-#{key.to_s.dasherize}"
314
311
  unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
@@ -92,6 +92,7 @@ module ActionView
92
92
  end
93
93
  end
94
94
  end
95
+ alias_method :add_default_name_and_id_for_value, :add_default_name_and_field_for_value
95
96
 
96
97
  def add_default_name_and_field(options, field = "id")
97
98
  index = name_and_id_index(options)
@@ -104,6 +105,7 @@ module ActionView
104
105
  end
105
106
  end
106
107
  end
108
+ alias_method :add_default_name_and_id, :add_default_name_and_field
107
109
 
108
110
  def tag_name(multiple = false, index = nil)
109
111
  @template_object.field_name(@object_name, sanitized_method_name, multiple: multiple, index: index)
@@ -57,7 +57,13 @@ module ActionView
57
57
  end
58
58
 
59
59
  def hidden_field_for_checkbox(options)
60
- @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value, "autocomplete" => "off")) : "".html_safe
60
+ if @unchecked_value
61
+ tag_options = options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)
62
+ tag_options["autocomplete"] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
63
+ tag("input", tag_options)
64
+ else
65
+ "".html_safe
66
+ end
61
67
  end
62
68
  end
63
69
  end
@@ -6,7 +6,7 @@ module ActionView
6
6
  class DatetimeField < TextField # :nodoc:
7
7
  def render
8
8
  options = @options.stringify_keys
9
- options["value"] = datetime_value(options["value"] || value)
9
+ options["value"] = datetime_value(options.fetch("value", value))
10
10
  options["min"] = format_datetime(parse_datetime(options["min"]))
11
11
  options["max"] = format_datetime(parse_datetime(options["max"]))
12
12
  @options = options
@@ -18,7 +18,9 @@ module ActionView
18
18
 
19
19
  private
20
20
  def hidden_field_for_multiple_file(options)
21
- tag("input", "name" => options["name"], "type" => "hidden", "value" => "", "autocomplete" => "off")
21
+ tag_options = { "name" => options["name"], "type" => "hidden", "value" => "" }
22
+ tag_options["autocomplete"] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
23
+ tag("input", tag_options)
22
24
  end
23
25
  end
24
26
  end
@@ -5,7 +5,7 @@ module ActionView
5
5
  module Tags # :nodoc:
6
6
  class HiddenField < TextField # :nodoc:
7
7
  def render
8
- @options[:autocomplete] = "off"
8
+ @options.reverse_merge!(autocomplete: "off")
9
9
  super
10
10
  end
11
11
  end
@@ -37,7 +37,12 @@ module ActionView
37
37
  # [nil, []]
38
38
  # { nil => [] }
39
39
  def grouped_choices?
40
- !@choices.blank? && @choices.first.respond_to?(:second) && Array === @choices.first.second
40
+ return false if @choices.blank?
41
+
42
+ first_choice = @choices.first
43
+ return false unless first_choice.is_a?(Enumerable)
44
+
45
+ first_choice.second.is_a?(Array)
41
46
  end
42
47
  end
43
48
  end
@@ -22,7 +22,9 @@ module ActionView
22
22
  select = content_tag("select", add_options(option_tags, options, value), html_options)
23
23
 
24
24
  if html_options["multiple"] && options.fetch(:include_hidden, true)
25
- tag("input", disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "", autocomplete: "off") + select
25
+ tag_options = { disabled: html_options["disabled"], name: html_options["name"], type: "hidden", value: "" }
26
+ tag_options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
27
+ tag("input", tag_options) + select
26
28
  else
27
29
  select
28
30
  end
@@ -37,7 +39,7 @@ module ActionView
37
39
  if options[:include_blank]
38
40
  content = (options[:include_blank] if options[:include_blank].is_a?(String))
39
41
  label = (" " unless content)
40
- option_tags = tag_builder.content_tag_string("option", content, value: "", label: label) + "\n" + option_tags
42
+ option_tags = tag_builder.option(content, value: "", label: label) + "\n" + option_tags
41
43
  end
42
44
 
43
45
  if value.blank? && options[:prompt]
@@ -45,7 +47,7 @@ module ActionView
45
47
  prompt_opts[:disabled] = true if options[:disabled] == ""
46
48
  prompt_opts[:selected] = true if options[:selected] == ""
47
49
  end
48
- option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
50
+ option_tags = tag_builder.option(prompt_text(options[:prompt]), **tag_options) + "\n" + option_tags
49
51
  end
50
52
 
51
53
  option_tags
@@ -140,7 +140,12 @@ module ActionView
140
140
  end
141
141
 
142
142
  def missing_translation(key, options)
143
- keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
143
+ locale = options[:locale] || I18n.locale
144
+
145
+ i18n_exception = I18n::MissingTranslation.new(locale, key, options)
146
+ I18n.exception_handler.call(i18n_exception, locale, key, options)
147
+
148
+ keys = I18n.normalize_keys(locale, key, options[:scope])
144
149
 
145
150
  title = +"translation missing: #{keys.join(".")}"
146
151
 
@@ -341,8 +341,9 @@ module ActionView
341
341
  inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
342
342
  if params
343
343
  to_form_params(params).each do |param|
344
- inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value],
345
- autocomplete: "off")
344
+ options = { type: "hidden", name: param[:name], value: param[:value] }
345
+ options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
346
+ inner_tags.safe_concat tag(:input, **options)
346
347
  end
347
348
  end
348
349
  html = content_tag("form", inner_tags, form_options)
@@ -538,24 +539,47 @@ module ActionView
538
539
  # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
539
540
  # # => true
540
541
  #
541
- # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
542
+ # Different actions may share the same URL path but have a different HTTP method. Let's say we
543
+ # sent a POST to <tt>http://www.example.com/products</tt> and rendered a validation error.
542
544
  #
543
545
  # current_page?(controller: 'product', action: 'index')
544
546
  # # => false
545
547
  #
548
+ # current_page?(controller: 'product', action: 'create')
549
+ # # => false
550
+ #
551
+ # current_page?(controller: 'product', action: 'create', method: :post)
552
+ # # => true
553
+ #
554
+ # current_page?(controller: 'product', action: 'index', method: [:get, :post])
555
+ # # => true
556
+ #
546
557
  # We can also pass in the symbol arguments instead of strings.
547
558
  #
548
- def current_page?(options = nil, check_parameters: false, **options_as_kwargs)
559
+ def current_page?(options = nil, check_parameters: false, method: :get, **options_as_kwargs)
549
560
  unless request
550
561
  raise "You cannot use helpers that need to determine the current " \
551
562
  "page unless your view context provides a Request object " \
552
563
  "in a #request method"
553
564
  end
554
565
 
555
- return false unless request.get? || request.head?
566
+ if options.is_a?(Hash)
567
+ check_parameters = options.delete(:check_parameters) { check_parameters }
568
+ method = options.delete(:method) { method }
569
+ else
570
+ options ||= options_as_kwargs
571
+ end
572
+
573
+ method_matches = case method
574
+ when :get
575
+ request.get? || request.head?
576
+ when Array
577
+ method.include?(request.method_symbol) || (method.include?(:get) && request.head?)
578
+ else
579
+ method == request.method_symbol
580
+ end
581
+ return false unless method_matches
556
582
 
557
- options ||= options_as_kwargs
558
- check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
559
583
  url_string = URI::RFC2396_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
560
584
 
561
585
  # We ignore any extra parameters in the request_uri if the
@@ -751,14 +775,18 @@ module ActionView
751
775
  else
752
776
  token
753
777
  end
754
- tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token, autocomplete: "off")
778
+ options = { type: "hidden", name: request_forgery_protection_token.to_s, value: token }
779
+ options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
780
+ tag(:input, **options)
755
781
  else
756
782
  ""
757
783
  end
758
784
  end
759
785
 
760
786
  def method_tag(method)
761
- tag("input", type: "hidden", name: "_method", value: method.to_s, autocomplete: "off")
787
+ options = { type: "hidden", name: "_method", value: method.to_s }
788
+ options[:autocomplete] = "off" unless ActionView::Base.remove_hidden_field_autocomplete
789
+ tag("input", **options)
762
790
  end
763
791
 
764
792
  # Returns an array of hashes each containing :name and :value keys
@@ -43,6 +43,9 @@
43
43
  hour: "Hour"
44
44
  minute: "Minute"
45
45
  second: "Seconds"
46
+ relative:
47
+ future: "in %{time}"
48
+ past: "%{time} ago"
46
49
 
47
50
  helpers:
48
51
  select:
@@ -3,10 +3,7 @@
3
3
  require "active_support/log_subscriber"
4
4
 
5
5
  module ActionView
6
- # = Action View Log Subscriber
7
- #
8
- # Provides functionality so that \Rails can output logs from Action View.
9
- class LogSubscriber < ActiveSupport::LogSubscriber
6
+ class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
10
7
  VIEWS_PATTERN = /^app\/views\//
11
8
 
12
9
  def initialize
@@ -72,8 +72,19 @@ module ActionView
72
72
  end
73
73
 
74
74
  config.after_initialize do |app|
75
+ ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_scripts = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["script-src", "script-src-elem", "script-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
76
+ ActionView::Helpers::AssetTagHelper.auto_include_nonce_for_styles = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["style-src", "style-src-elem", "style-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
77
+ ActionView::Helpers::JavaScriptHelper.auto_include_nonce = app.config.content_security_policy_nonce_auto && app.config.content_security_policy_nonce_directives.intersect?(["script-src", "script-src-elem", "script-src-attr"]) && app.config.content_security_policy_nonce_generator.present?
78
+ end
79
+
80
+ config.after_initialize do |app|
81
+ config.after_initialize do
82
+ ActionView.render_tracker = config.action_view.render_tracker
83
+ end
84
+
75
85
  ActiveSupport.on_load(:action_view) do
76
86
  app.config.action_view.each do |k, v|
87
+ next if k == :render_tracker
77
88
  send "#{k}=", v
78
89
  end
79
90
  end
@@ -101,6 +101,27 @@ module ActionView
101
101
  end
102
102
  end
103
103
 
104
+ # The DOM target convention is to concatenate any number of parameters into a string.
105
+ # Records are passed through dom_id, while string and symbols are retained.
106
+ #
107
+ # dom_target(Post.find(45)) # => "post_45"
108
+ # dom_target(Post.find(45), :edit) # => "post_45_edit"
109
+ # dom_target(Post.find(45), :edit, :special) # => "post_45_edit_special"
110
+ # dom_target(Post.find(45), Comment.find(1)) # => "post_45_comment_1"
111
+ def dom_target(*objects)
112
+ objects.map! do |object|
113
+ case object
114
+ when Symbol, String
115
+ object
116
+ when Class
117
+ dom_class(object)
118
+ else
119
+ dom_id(object)
120
+ end
121
+ end
122
+ objects.join(JOIN)
123
+ end
124
+
104
125
  private
105
126
  # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
106
127
  # This can be overwritten to customize the default generated string representation if desired.
@@ -48,6 +48,22 @@ module ActionView
48
48
  #
49
49
  # <%= render partial: "account", locals: { user: @buyer } %>
50
50
  #
51
+ # == \Rendering variants of a partial
52
+ #
53
+ # The <tt>:variants</tt> option can be used to render a different template variant of a partial. For instance:
54
+ #
55
+ # <%= render partial: "account", variants: :mobile %>
56
+ #
57
+ # This will render <tt>_account.html+mobile.erb</tt>. This option also accepts multiple variants
58
+ # like so:
59
+ #
60
+ # <%= render partial: "account", variants: [:desktop, :mobile] %>
61
+ #
62
+ # This will look for the following templates and render the first one that exists:
63
+ # * <tt>_account.html+desktop.erb</tt>
64
+ # * <tt>_account.html+mobile.erb</tt>
65
+ # * <tt>_account.html.erb</tt>
66
+ #
51
67
  # == \Rendering a collection of partials
52
68
  #
53
69
  # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/structured_event_subscriber"
4
+
5
+ module ActionView
6
+ class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
7
+ VIEWS_PATTERN = /^app\/views\//
8
+
9
+ def initialize
10
+ @root = nil
11
+ super
12
+ end
13
+
14
+ def render_template(event)
15
+ emit_debug_event("action_view.render_template",
16
+ identifier: from_rails_root(event.payload[:identifier]),
17
+ layout: from_rails_root(event.payload[:layout]),
18
+ duration_ms: event.duration.round(2),
19
+ gc_ms: event.gc_time.round(2),
20
+ )
21
+ end
22
+ debug_only :render_template
23
+
24
+ def render_partial(event)
25
+ emit_debug_event("action_view.render_partial",
26
+ identifier: from_rails_root(event.payload[:identifier]),
27
+ layout: from_rails_root(event.payload[:layout]),
28
+ duration_ms: event.duration.round(2),
29
+ gc_ms: event.gc_time.round(2),
30
+ cache_hit: event.payload[:cache_hit],
31
+ )
32
+ end
33
+ debug_only :render_partial
34
+
35
+ def render_layout(event)
36
+ emit_event("action_view.render_layout",
37
+ identifier: from_rails_root(event.payload[:identifier]),
38
+ duration_ms: event.duration.round(2),
39
+ gc_ms: event.gc_time.round(2),
40
+ )
41
+ end
42
+ debug_only :render_layout
43
+
44
+ def render_collection(event)
45
+ emit_debug_event("action_view.render_collection",
46
+ identifier: from_rails_root(event.payload[:identifier] || "templates"),
47
+ layout: from_rails_root(event.payload[:layout]),
48
+ duration_ms: event.duration.round(2),
49
+ gc_ms: event.gc_time.round(2),
50
+ cache_hits: event.payload[:cache_hits],
51
+ count: event.payload[:count],
52
+ )
53
+ end
54
+ debug_only :render_collection
55
+
56
+ module Utils # :nodoc:
57
+ private
58
+ def from_rails_root(string)
59
+ return unless string
60
+
61
+ string = string.sub("#{rails_root}/", "")
62
+ string.sub!(VIEWS_PATTERN, "")
63
+ string
64
+ end
65
+
66
+ def rails_root # :doc:
67
+ @root ||= Rails.try(:root)
68
+ end
69
+ end
70
+
71
+ include Utils
72
+
73
+ class Start # :nodoc:
74
+ include Utils
75
+
76
+ def start(name, id, payload)
77
+ ActiveSupport.event_reporter.debug("action_view.render_start",
78
+ is_layout: name == "render_layout.action_view",
79
+ identifier: from_rails_root(payload[:identifier]),
80
+ layout: from_rails_root(payload[:layout]),
81
+ )
82
+ end
83
+
84
+ def finish(name, id, payload)
85
+ end
86
+ end
87
+
88
+ def self.attach_to(*)
89
+ ActiveSupport::Notifications.subscribe("render_template.action_view", Start.new)
90
+ ActiveSupport::Notifications.subscribe("render_layout.action_view", Start.new)
91
+
92
+ super
93
+ end
94
+ end
95
+ end
96
+
97
+ ActionView::StructuredEventSubscriber.attach_to :action_view
@@ -260,9 +260,13 @@ module ActionView
260
260
  end
261
261
 
262
262
  def message
263
- <<~MESSAGE
264
- Encountered a syntax error while rendering template: check #{@offending_code_string}
265
- MESSAGE
263
+ if template.is_a?(Template::Inline)
264
+ <<~MESSAGE
265
+ Encountered a syntax error while rendering template: check #{@offending_code_string}
266
+ MESSAGE
267
+ else
268
+ "Encountered a syntax error while rendering template located at: #{template.short_identifier}"
269
+ end
266
270
  end
267
271
 
268
272
  def annotated_source_code