actionview 7.2.3 → 8.1.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -121
  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 +37 -28
  6. data/lib/action_view/dependency_tracker/ruby_tracker.rb +2 -19
  7. data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
  8. data/lib/action_view/dependency_tracker.rb +7 -1
  9. data/lib/action_view/gem_version.rb +2 -2
  10. data/lib/action_view/helpers/asset_tag_helper.rb +21 -2
  11. data/lib/action_view/helpers/atom_feed_helper.rb +0 -2
  12. data/lib/action_view/helpers/cache_helper.rb +8 -0
  13. data/lib/action_view/helpers/capture_helper.rb +2 -2
  14. data/lib/action_view/helpers/controller_helper.rb +6 -2
  15. data/lib/action_view/helpers/date_helper.rb +20 -3
  16. data/lib/action_view/helpers/form_helper.rb +76 -76
  17. data/lib/action_view/helpers/form_options_helper.rb +33 -32
  18. data/lib/action_view/helpers/form_tag_helper.rb +35 -25
  19. data/lib/action_view/helpers/javascript_helper.rb +5 -1
  20. data/lib/action_view/helpers/number_helper.rb +14 -0
  21. data/lib/action_view/helpers/rendering_helper.rb +160 -50
  22. data/lib/action_view/helpers/sanitize_helper.rb +6 -0
  23. data/lib/action_view/helpers/tag_helper.rb +62 -75
  24. data/lib/action_view/helpers/tags/base.rb +11 -9
  25. data/lib/action_view/helpers/tags/check_box.rb +9 -3
  26. data/lib/action_view/helpers/tags/collection_check_boxes.rb +4 -3
  27. data/lib/action_view/helpers/tags/datetime_field.rb +1 -1
  28. data/lib/action_view/helpers/tags/file_field.rb +7 -2
  29. data/lib/action_view/helpers/tags/hidden_field.rb +1 -1
  30. data/lib/action_view/helpers/tags/label.rb +3 -10
  31. data/lib/action_view/helpers/tags/radio_button.rb +1 -1
  32. data/lib/action_view/helpers/tags/select.rb +6 -1
  33. data/lib/action_view/helpers/tags/select_renderer.rb +6 -4
  34. data/lib/action_view/helpers/tags/text_area.rb +1 -1
  35. data/lib/action_view/helpers/tags/text_field.rb +1 -1
  36. data/lib/action_view/helpers/translation_helper.rb +6 -1
  37. data/lib/action_view/helpers/url_helper.rb +39 -13
  38. data/lib/action_view/layouts.rb +1 -1
  39. data/lib/action_view/locale/en.yml +3 -0
  40. data/lib/action_view/log_subscriber.rb +1 -4
  41. data/lib/action_view/railtie.rb +12 -1
  42. data/lib/action_view/record_identifier.rb +22 -1
  43. data/lib/action_view/render_parser/prism_render_parser.rb +13 -1
  44. data/lib/action_view/render_parser/ripper_render_parser.rb +10 -1
  45. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +5 -1
  46. data/lib/action_view/renderer/partial_renderer.rb +16 -0
  47. data/lib/action_view/renderer/streaming_template_renderer.rb +8 -2
  48. data/lib/action_view/rendering.rb +2 -3
  49. data/lib/action_view/structured_event_subscriber.rb +97 -0
  50. data/lib/action_view/template/error.rb +7 -3
  51. data/lib/action_view/template/handlers/erb/erubi.rb +1 -1
  52. data/lib/action_view/template/handlers/erb.rb +37 -12
  53. data/lib/action_view/template/raw_file.rb +4 -0
  54. data/lib/action_view/template/resolver.rb +0 -1
  55. data/lib/action_view/template.rb +9 -4
  56. data/lib/action_view/test_case.rb +50 -52
  57. data/lib/action_view.rb +3 -0
  58. metadata +14 -26
@@ -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
@@ -11,7 +11,7 @@ module ActionView
11
11
  html_options[prop.to_s] = options.delete(prop) if options.key?(prop) && !html_options.key?(prop.to_s)
12
12
  end
13
13
 
14
- add_default_name_and_id(html_options)
14
+ add_default_name_and_field(html_options)
15
15
 
16
16
  if placeholder_required?(html_options)
17
17
  raise ArgumentError, "include_blank cannot be false for a required field." if options[:include_blank] == false
@@ -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
@@ -10,7 +10,7 @@ module ActionView
10
10
 
11
11
  def render
12
12
  options = @options.stringify_keys
13
- add_default_name_and_id(options)
13
+ add_default_name_and_field(options)
14
14
 
15
15
  if size = options.delete("size")
16
16
  options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
@@ -13,7 +13,7 @@ module ActionView
13
13
  options["size"] = options["maxlength"] unless options.key?("size")
14
14
  options["type"] ||= field_type
15
15
  options["value"] = options.fetch("value") { value_before_type_cast } unless field_type == "file"
16
- add_default_name_and_id(options)
16
+ add_default_name_and_field(options)
17
17
  tag("input", options)
18
18
  end
19
19
 
@@ -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)
@@ -500,8 +501,6 @@ module ActionView
500
501
  content_tag("a", name || email_address, html_options, &block)
501
502
  end
502
503
 
503
- RFC2396_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new
504
-
505
504
  # True if the current request URI was generated by the given +options+.
506
505
  #
507
506
  # ==== Examples
@@ -540,32 +539,55 @@ module ActionView
540
539
  # current_page?('http://www.example.com/shop/checkout?order=desc&page=1')
541
540
  # # => true
542
541
  #
543
- # 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.
544
544
  #
545
545
  # current_page?(controller: 'product', action: 'index')
546
546
  # # => false
547
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
+ #
548
557
  # We can also pass in the symbol arguments instead of strings.
549
558
  #
550
- 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)
551
560
  unless request
552
561
  raise "You cannot use helpers that need to determine the current " \
553
562
  "page unless your view context provides a Request object " \
554
563
  "in a #request method"
555
564
  end
556
565
 
557
- 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
558
582
 
559
- options ||= options_as_kwargs
560
- check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
561
- url_string = RFC2396_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
583
+ url_string = URI::RFC2396_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
562
584
 
563
585
  # We ignore any extra parameters in the request_uri if the
564
586
  # submitted URL doesn't have any either. This lets the function
565
587
  # work with things like ?order=asc
566
588
  # the behavior can be disabled with check_parameters: true
567
589
  request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
568
- request_uri = RFC2396_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
590
+ request_uri = URI::RFC2396_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
569
591
 
570
592
  if %r{^\w+://}.match?(url_string)
571
593
  request_uri = +"#{request.protocol}#{request.host_with_port}#{request_uri}"
@@ -753,14 +775,18 @@ module ActionView
753
775
  else
754
776
  token
755
777
  end
756
- 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)
757
781
  else
758
782
  ""
759
783
  end
760
784
  end
761
785
 
762
786
  def method_tag(method)
763
- 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)
764
790
  end
765
791
 
766
792
  # Returns an array of hashes each containing :name and :value keys
@@ -347,7 +347,7 @@ module ActionView
347
347
  end
348
348
  end
349
349
 
350
- def _normalize_options(options) # :nodoc:
350
+ def _process_render_template_options(options) # :nodoc:
351
351
  super
352
352
 
353
353
  if _include_layout?(options)
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_view"
4
3
  require "rails"
4
+ require "action_view"
5
5
 
6
6
  module ActionView
7
7
  # = Action View Railtie
@@ -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
@@ -11,7 +11,7 @@ module ActionView
11
11
  #
12
12
  # Consider for example the following code that form of post:
13
13
  #
14
- # <%= form_for(post) do |f| %>
14
+ # <%= form_with(model: post) do |f| %>
15
15
  # <%= f.text_field :body %>
16
16
  # <% end %>
17
17
  #
@@ -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.
@@ -97,9 +97,21 @@ module ActionView
97
97
  def render_call_template(node)
98
98
  object_template = false
99
99
  template =
100
- if node.is_a?(Prism::StringNode)
100
+ case node.type
101
+ when :string_node
101
102
  path = node.unescaped
102
103
  path.include?("/") ? path : "#{directory}/#{path}"
104
+ when :interpolated_string_node
105
+ node.parts.map do |node|
106
+ case node.type
107
+ when :string_node
108
+ node.unescaped
109
+ when :embedded_statements_node
110
+ "*"
111
+ else
112
+ return
113
+ end
114
+ end.join("")
103
115
  else
104
116
  dependency =
105
117
  case node.type
@@ -66,7 +66,16 @@ module ActionView
66
66
 
67
67
  def to_string
68
68
  raise unless string?
69
- self[0][0][0]
69
+
70
+ # s(:string_literal, s(:string_content, map))
71
+ self[0].map do |node|
72
+ case node.type
73
+ when :@tstring_content
74
+ node[0]
75
+ when :string_embexpr
76
+ "*"
77
+ end
78
+ end.join("")
70
79
  end
71
80
 
72
81
  def hash?
@@ -111,7 +111,11 @@ module ActionView
111
111
  end
112
112
 
113
113
  unless entries_to_write.empty?
114
- collection_cache.write_multi(entries_to_write)
114
+ if @options[:cached].is_a?(Hash) && @options[:cached].key?(:expires_in)
115
+ collection_cache.write_multi(entries_to_write, expires_in: @options[:cached][:expires_in])
116
+ else
117
+ collection_cache.write_multi(entries_to_write)
118
+ end
115
119
  end
116
120
 
117
121
  keyed_partials
@@ -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
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fiber"
4
3
 
5
4
  module ActionView
6
5
  # == TODO
@@ -26,6 +25,13 @@ module ActionView
26
25
  self
27
26
  end
28
27
 
28
+ # Returns the complete body as a string.
29
+ def body
30
+ buffer = String.new
31
+ each { |part| buffer << part }
32
+ buffer
33
+ end
34
+
29
35
  private
30
36
  # This is the same logging logic as in ShowExceptions middleware.
31
37
  def log_error(exception)
@@ -43,7 +49,7 @@ module ActionView
43
49
  # object that responds to each. This object is initialized with a block
44
50
  # that knows how to render the template.
45
51
  def render_template(view, template, layout_name = nil, locals = {}) # :nodoc:
46
- return [super.body] unless layout_name && template.supports_streaming?
52
+ return [super.body] unless template.supports_streaming?
47
53
 
48
54
  locals ||= {}
49
55
  layout = find_layout(layout_name, locals.keys, [formats.first])
@@ -118,6 +118,7 @@ module ActionView
118
118
 
119
119
  def render_to_body(options = {})
120
120
  _process_options(options)
121
+ _process_render_template_options(options)
121
122
  _render_template(options)
122
123
  end
123
124
 
@@ -173,8 +174,7 @@ module ActionView
173
174
  end
174
175
 
175
176
  # Normalize options.
176
- def _normalize_options(options)
177
- options = super(options)
177
+ def _process_render_template_options(options)
178
178
  if options[:partial] == true
179
179
  options[:partial] = action_name
180
180
  end
@@ -184,7 +184,6 @@ module ActionView
184
184
  end
185
185
 
186
186
  options[:template] ||= (options[:action] || action_name).to_s
187
- options
188
187
  end
189
188
  end
190
189
  end
@@ -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
@@ -18,7 +18,7 @@ module ActionView
18
18
  properties[:preamble] ||= ""
19
19
  properties[:postamble] ||= "#{properties[:bufvar]}"
20
20
 
21
- # Tell Eruby that whether template will be compiled with `frozen_string_literal: true`
21
+ # Tell Erubi whether the template will be compiled with `frozen_string_literal: true`
22
22
  properties[:freeze_template_literals] = !Template.frozen_string_literal
23
23
 
24
24
  properties[:escapefunc] = ""
@@ -40,17 +40,19 @@ module ActionView
40
40
 
41
41
  # Translate an error location returned by ErrorHighlight to the correct
42
42
  # source location inside the template.
43
- def translate_location(spot, backtrace_location, source)
44
- # Tokenize the source line
43
+ def translate_location(spot, _backtrace_location, source)
44
+ compiled = spot[:script_lines]
45
+ highlight = compiled[spot[:first_lineno] - 1]&.byteslice((spot[:first_column] - 1)...spot[:last_column])
46
+ return nil if highlight.blank?
47
+
45
48
  source_lines = source.lines
46
- return nil if source_lines.size < backtrace_location.lineno
47
- tokens = ::ERB::Util.tokenize(source_lines[backtrace_location.lineno - 1])
48
- new_first_column = find_offset(spot[:snippet], tokens, spot[:first_column])
49
- lineno_delta = spot[:first_lineno] - backtrace_location.lineno
49
+ lineno_delta = find_lineno_offset(compiled, source_lines, highlight, spot[:first_lineno])
50
+
51
+ tokens = ::ERB::Util.tokenize(source_lines[spot[:first_lineno] - lineno_delta - 1])
52
+ column_delta = find_offset(spot[:snippet], tokens, spot[:first_column])
53
+
50
54
  spot[:first_lineno] -= lineno_delta
51
55
  spot[:last_lineno] -= lineno_delta
52
-
53
- column_delta = spot[:first_column] - new_first_column
54
56
  spot[:first_column] -= column_delta
55
57
  spot[:last_column] -= column_delta
56
58
  spot[:script_lines] = source_lines
@@ -84,7 +86,7 @@ module ActionView
84
86
  }
85
87
 
86
88
  if ActionView::Base.annotate_rendered_view_with_filenames && template.format == :html
87
- options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier} -->';"
89
+ options[:preamble] = "@output_buffer.safe_append='<!-- BEGIN #{template.short_identifier}\n-->';"
88
90
  options[:postamble] = "@output_buffer.safe_append='<!-- END #{template.short_identifier} -->';@output_buffer"
89
91
  end
90
92
 
@@ -107,6 +109,28 @@ module ActionView
107
109
  raise WrongEncodingError.new(string, string.encoding)
108
110
  end
109
111
 
112
+ # Return the offset between the error lineno and the source lineno.
113
+ # Searches in reverse from the backtrace lineno so we have a better
114
+ # chance of finding the correct line
115
+ #
116
+ # The compiled template is likely to be longer than the source.
117
+ # Use the difference between the compiled and source sizes to
118
+ # determine the earliest line that could contain the highlight.
119
+ def find_lineno_offset(compiled, source_lines, highlight, error_lineno)
120
+ first_index = error_lineno - 1 - compiled.size + source_lines.size
121
+ first_index = 0 if first_index < 0
122
+
123
+ last_index = error_lineno - 1
124
+ last_index = source_lines.size - 1 if last_index >= source_lines.size
125
+
126
+ last_index.downto(first_index) do |line_index|
127
+ next unless source_lines[line_index].include?(highlight)
128
+ return error_lineno - 1 - line_index
129
+ end
130
+
131
+ raise LocationParsingError, "Couldn't find code snippet"
132
+ end
133
+
110
134
  # Find which token in the source template spans the byte range that
111
135
  # contains the error_column, then return the offset compared to the
112
136
  # original source template.
@@ -137,7 +161,7 @@ module ActionView
137
161
  matched_str = true
138
162
 
139
163
  if name == :CODE && compiled.pos <= error_column && compiled.pos + str.bytesize >= error_column
140
- return error_column - compiled.pos + offset
164
+ return compiled.pos - offset
141
165
  end
142
166
 
143
167
  compiled.pos += str.bytesize
@@ -152,8 +176,9 @@ module ActionView
152
176
 
153
177
  def offset_source_tokens(source_tokens)
154
178
  source_offset = 0
155
- with_offset = source_tokens.filter_map do |(name, str)|
156
- result = [name, str, source_offset] if name == :CODE || name == :TEXT
179
+ with_offset = source_tokens.filter_map do |name, str|
180
+ result = [:CODE, str, source_offset] if name == :CODE || name == :PLAIN
181
+ result = [:TEXT, str, source_offset] if name == :TEXT
157
182
  source_offset += str.bytesize
158
183
  result
159
184
  end
@@ -20,6 +20,10 @@ module ActionView # :nodoc:
20
20
  def render(*args)
21
21
  ::File.read(@filename)
22
22
  end
23
+
24
+ def supports_streaming?
25
+ false
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -4,7 +4,6 @@ require "pathname"
4
4
  require "active_support/core_ext/class"
5
5
  require "active_support/core_ext/module/attribute_accessors"
6
6
  require "action_view/template"
7
- require "thread"
8
7
  require "concurrent/map"
9
8
 
10
9
  module ActionView