actionview 7.0.8.1 → 7.2.2.1

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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -425
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +52 -14
  8. data/lib/action_view/buffers.rb +106 -8
  9. data/lib/action_view/cache_expiry.rb +44 -41
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/dependency_tracker/{ripper_tracker.rb → ruby_tracker.rb} +4 -3
  12. data/lib/action_view/dependency_tracker.rb +1 -1
  13. data/lib/action_view/deprecator.rb +7 -0
  14. data/lib/action_view/digestor.rb +1 -1
  15. data/lib/action_view/gem_version.rb +3 -3
  16. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  17. data/lib/action_view/helpers/asset_tag_helper.rb +151 -55
  18. data/lib/action_view/helpers/asset_url_helper.rb +6 -5
  19. data/lib/action_view/helpers/atom_feed_helper.rb +5 -5
  20. data/lib/action_view/helpers/cache_helper.rb +7 -13
  21. data/lib/action_view/helpers/capture_helper.rb +30 -10
  22. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  23. data/lib/action_view/helpers/controller_helper.rb +6 -0
  24. data/lib/action_view/helpers/csp_helper.rb +2 -2
  25. data/lib/action_view/helpers/csrf_helper.rb +3 -3
  26. data/lib/action_view/helpers/date_helper.rb +17 -19
  27. data/lib/action_view/helpers/debug_helper.rb +3 -3
  28. data/lib/action_view/helpers/form_helper.rb +248 -214
  29. data/lib/action_view/helpers/form_options_helper.rb +2 -1
  30. data/lib/action_view/helpers/form_tag_helper.rb +125 -58
  31. data/lib/action_view/helpers/javascript_helper.rb +1 -0
  32. data/lib/action_view/helpers/number_helper.rb +37 -330
  33. data/lib/action_view/helpers/output_safety_helper.rb +6 -6
  34. data/lib/action_view/helpers/rendering_helper.rb +1 -1
  35. data/lib/action_view/helpers/sanitize_helper.rb +51 -21
  36. data/lib/action_view/helpers/tag_helper.rb +210 -42
  37. data/lib/action_view/helpers/tags/base.rb +11 -52
  38. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  39. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  40. data/lib/action_view/helpers/tags/collection_select.rb +3 -0
  41. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  42. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  43. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  45. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  46. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  47. data/lib/action_view/helpers/tags/select.rb +3 -0
  48. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  49. data/lib/action_view/helpers/tags/time_field.rb +1 -1
  50. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  51. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  52. data/lib/action_view/helpers/tags/weekday_select.rb +3 -0
  53. data/lib/action_view/helpers/tags.rb +2 -0
  54. data/lib/action_view/helpers/text_helper.rb +157 -85
  55. data/lib/action_view/helpers/translation_helper.rb +3 -3
  56. data/lib/action_view/helpers/url_helper.rb +35 -80
  57. data/lib/action_view/helpers.rb +2 -0
  58. data/lib/action_view/layouts.rb +8 -8
  59. data/lib/action_view/log_subscriber.rb +57 -36
  60. data/lib/action_view/lookup_context.rb +29 -13
  61. data/lib/action_view/path_registry.rb +57 -0
  62. data/lib/action_view/path_set.rb +13 -14
  63. data/lib/action_view/railtie.rb +25 -3
  64. data/lib/action_view/record_identifier.rb +15 -8
  65. data/lib/action_view/render_parser/prism_render_parser.rb +127 -0
  66. data/lib/action_view/render_parser/ripper_render_parser.rb +341 -0
  67. data/lib/action_view/render_parser.rb +21 -169
  68. data/lib/action_view/renderer/abstract_renderer.rb +2 -2
  69. data/lib/action_view/renderer/collection_renderer.rb +10 -2
  70. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +2 -1
  71. data/lib/action_view/renderer/partial_renderer.rb +2 -1
  72. data/lib/action_view/renderer/renderer.rb +34 -38
  73. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -2
  74. data/lib/action_view/renderer/template_renderer.rb +3 -2
  75. data/lib/action_view/rendering.rb +26 -8
  76. data/lib/action_view/template/error.rb +14 -1
  77. data/lib/action_view/template/handlers/builder.rb +4 -4
  78. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  79. data/lib/action_view/template/handlers/erb.rb +73 -1
  80. data/lib/action_view/template/handlers.rb +1 -1
  81. data/lib/action_view/template/html.rb +1 -1
  82. data/lib/action_view/template/raw_file.rb +1 -1
  83. data/lib/action_view/template/renderable.rb +8 -2
  84. data/lib/action_view/template/resolver.rb +9 -3
  85. data/lib/action_view/template/text.rb +1 -1
  86. data/lib/action_view/template/types.rb +25 -34
  87. data/lib/action_view/template.rb +278 -55
  88. data/lib/action_view/template_path.rb +2 -0
  89. data/lib/action_view/test_case.rb +181 -28
  90. data/lib/action_view/unbound_template.rb +17 -7
  91. data/lib/action_view/version.rb +1 -1
  92. data/lib/action_view/view_paths.rb +15 -24
  93. data/lib/action_view.rb +4 -1
  94. metadata +31 -31
  95. data/lib/action_view/ripper_ast_parser.rb +0 -198
  96. data/lib/assets/compiled/rails-ujs.js +0 -777
@@ -3,11 +3,13 @@
3
3
  require "active_support/core_ext/array/access"
4
4
  require "active_support/core_ext/hash/keys"
5
5
  require "active_support/core_ext/string/output_safety"
6
+ require "action_view/helpers/content_exfiltration_prevention_helper"
6
7
  require "action_view/helpers/tag_helper"
7
8
 
8
9
  module ActionView
9
- # = Action View URL Helpers
10
10
  module Helpers # :nodoc:
11
+ # = Action View URL \Helpers
12
+ #
11
13
  # Provides a set of methods for making links and getting URLs that
12
14
  # depend on the routing subsystem (see ActionDispatch::Routing).
13
15
  # This allows you to use the same format for links in views
@@ -22,6 +24,7 @@ module ActionView
22
24
  extend ActiveSupport::Concern
23
25
 
24
26
  include TagHelper
27
+ include ContentExfiltrationPreventionHelper
25
28
 
26
29
  module ClassMethods
27
30
  def _url_for_modules
@@ -93,7 +96,7 @@ module ActionView
93
96
  # ==== Examples
94
97
  #
95
98
  # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
96
- # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base
99
+ # and newer RESTful routes. Current \Rails style favors RESTful routes whenever possible, so base
97
100
  # your application on resources and use
98
101
  #
99
102
  # link_to "Profile", profile_path(@profile)
@@ -170,41 +173,27 @@ module ActionView
170
173
  # link_to "External link", "http://www.rubyonrails.org/", target: "_blank", rel: "nofollow"
171
174
  # # => <a href="http://www.rubyonrails.org/" target="_blank" rel="nofollow">External link</a>
172
175
  #
173
- # ==== Deprecated: Rails UJS Attributes
176
+ # ==== Turbo
174
177
  #
175
- # Prior to Rails 7, Rails shipped with a JavaScript library called <tt>@rails/ujs</tt> on by default. Following Rails 7,
176
- # this library is no longer on by default. This library integrated with the following options:
178
+ # Rails 7 ships with Turbo enabled by default. Turbo provides the following +:data+ options:
177
179
  #
178
- # * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically
179
- # create an HTML form and immediately submit the form for processing using
180
- # the HTTP verb specified. Useful for having links perform a POST operation
181
- # in dangerous actions like deleting a record (which search bots can follow
182
- # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
183
- # Note that if the user has JavaScript disabled, the request will fall back
184
- # to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
185
- # disabled clicking the link will have no effect. If you are relying on the
186
- # POST behavior, you should check for it in your controller's action by using
187
- # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>patch?</tt>, or <tt>put?</tt>.
188
- # * <tt>remote: true</tt> - This will allow <tt>@rails/ujs</tt>
189
- # to make an Ajax request to the URL in question instead of following
190
- # the link.
180
+ # * <tt>turbo_method: symbol of HTTP verb</tt> - Performs a Turbo link visit
181
+ # with the given HTTP verb. Forms are recommended when performing non-+GET+ requests.
182
+ # Only use <tt>data-turbo-method</tt> where a form is not possible.
191
183
  #
192
- # <tt>@rails/ujs</tt> also integrated with the following +:data+ options:
184
+ # * <tt>turbo_confirm: "question?"</tt> - Adds a confirmation dialog to the link with the
185
+ # given value.
193
186
  #
194
- # * <tt>confirm: "question?"</tt> - This will allow <tt>@rails/ujs</tt>
195
- # to prompt with the question specified (in this case, the
196
- # resulting text would be <tt>question?</tt>). If the user accepts, the
197
- # link is processed normally, otherwise no action is taken.
198
- # * <tt>:disable_with</tt> - Value of this parameter will be used as the
199
- # name for a disabled version of the link.
187
+ # {Consult the Turbo Handbook for more information on the options
188
+ # above.}[https://turbo.hotwired.dev/handbook/drive#performing-visits-with-a-different-method]
200
189
  #
201
- # ===== Rails UJS Examples
190
+ # ===== \Examples
202
191
  #
203
- # link_to "Remove Profile", profile_path(@profile), method: :delete
204
- # # => <a href="/profiles/1" rel="nofollow" data-method="delete">Remove Profile</a>
192
+ # link_to "Delete profile", @profile, data: { turbo_method: :delete }
193
+ # # => <a href="/profiles/1" data-turbo-method="delete">Delete profile</a>
205
194
  #
206
- # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
207
- # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
195
+ # link_to "Visit Other Site", "https://rubyonrails.org/", data: { turbo_confirm: "Are you sure?" }
196
+ # # => <a href="https://rubyonrails.org/" data-turbo-confirm="Are you sure?">Visit Other Site</a>
208
197
  #
209
198
  def link_to(name = nil, options = nil, html_options = nil, &block)
210
199
  html_options, options, name = options, name, block if block_given?
@@ -221,9 +210,6 @@ module ActionView
221
210
  # Generates a form containing a single button that submits to the URL created
222
211
  # by the set of +options+. This is the safest method to ensure links that
223
212
  # cause changes to your data are not triggered by search bots or accelerators.
224
- # If the HTML button does not work with your layout, you can also consider
225
- # using the +link_to+ method with the <tt>:method</tt> modifier as described in
226
- # the +link_to+ documentation.
227
213
  #
228
214
  # You can control the form and button behavior with +html_options+. Most
229
215
  # values in +html_options+ are passed through to the button element. For
@@ -234,8 +220,13 @@ module ActionView
234
220
  # +:form_class+ option within +html_options+. It defaults to
235
221
  # <tt>"button_to"</tt> to allow styling of the form and its children.
236
222
  #
237
- # The form submits a POST request by default. You can specify a different
238
- # 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+.
226
+ #
227
+ # If the HTML button generated from +button_to+ does not work with your layout, you can
228
+ # consider using the +link_to+ method with the +data-turbo-method+
229
+ # attribute as described in the +link_to+ documentation.
239
230
  #
240
231
  # ==== Options
241
232
  # The +options+ hash accepts the same options as +url_for+. To generate a
@@ -302,32 +293,6 @@ module ActionView
302
293
  # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6" autocomplete="off"/>
303
294
  # # </form>"
304
295
  #
305
- # ==== Deprecated: Rails UJS Attributes
306
- #
307
- # Prior to Rails 7, Rails shipped with a JavaScript library called <tt>@rails/ujs</tt> on by default. Following Rails 7,
308
- # this library is no longer on by default. This library integrated with the following options:
309
- #
310
- # * <tt>:remote</tt> - If set to true, will allow <tt>@rails/ujs</tt> to control the
311
- # submit behavior. By default this behavior is an Ajax submit.
312
- #
313
- # <tt>@rails/ujs</tt> also integrated with the following +:data+ options:
314
- #
315
- # * <tt>confirm: "question?"</tt> - This will allow <tt>@rails/ujs</tt>
316
- # to prompt with the question specified (in this case, the
317
- # resulting text would be <tt>question?</tt>). If the user accepts, the
318
- # button is processed normally, otherwise no action is taken.
319
- # * <tt>:disable_with</tt> - Value of this parameter will be
320
- # used as the value for a disabled version of the submit
321
- # button when the form is submitted.
322
- #
323
- # ===== Rails UJS Examples
324
- #
325
- # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
326
- # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
327
- # # <button type="submit">Create</button>
328
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6" autocomplete="off"/>
329
- # # </form>"
330
- #
331
296
  def button_to(name = nil, options = nil, html_options = nil, &block)
332
297
  html_options, options = options, name if block_given?
333
298
  html_options ||= {}
@@ -380,7 +345,8 @@ module ActionView
380
345
  autocomplete: "off")
381
346
  end
382
347
  end
383
- content_tag("form", inner_tags, form_options)
348
+ html = content_tag("form", inner_tags, form_options)
349
+ prevent_content_exfiltration(html)
384
350
  end
385
351
 
386
352
  # Creates a link tag of the given +name+ using a URL created by the set of
@@ -495,7 +461,7 @@ module ActionView
495
461
  # * <tt>:reply_to</tt> - Preset the +Reply-To+ field of the email.
496
462
  #
497
463
  # ==== Obfuscation
498
- # Prior to Rails 4.0, +mail_to+ provided options for encoding the address
464
+ # Prior to \Rails 4.0, +mail_to+ provided options for encoding the address
499
465
  # in order to hinder email harvesters. To take advantage of these options,
500
466
  # install the +actionview-encoded_mail_to+ gem.
501
467
  #
@@ -534,6 +500,8 @@ module ActionView
534
500
  content_tag("a", name || email_address, html_options, &block)
535
501
  end
536
502
 
503
+ RFC2396_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new
504
+
537
505
  # True if the current request URI was generated by the given +options+.
538
506
  #
539
507
  # ==== Examples
@@ -590,14 +558,14 @@ module ActionView
590
558
 
591
559
  options ||= options_as_kwargs
592
560
  check_parameters ||= options.is_a?(Hash) && options.delete(:check_parameters)
593
- url_string = URI::DEFAULT_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
561
+ url_string = RFC2396_PARSER.unescape(url_for(options)).force_encoding(Encoding::BINARY)
594
562
 
595
563
  # We ignore any extra parameters in the request_uri if the
596
564
  # submitted URL doesn't have any either. This lets the function
597
565
  # work with things like ?order=asc
598
- # the behaviour can be disabled with check_parameters: true
566
+ # the behavior can be disabled with check_parameters: true
599
567
  request_uri = url_string.index("?") || check_parameters ? request.fullpath : request.path
600
- request_uri = URI::DEFAULT_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
568
+ request_uri = RFC2396_PARSER.unescape(request_uri).force_encoding(Encoding::BINARY)
601
569
 
602
570
  if %r{^\w+://}.match?(url_string)
603
571
  request_uri = +"#{request.protocol}#{request.host_with_port}#{request_uri}"
@@ -609,19 +577,6 @@ module ActionView
609
577
  url_string == request_uri
610
578
  end
611
579
 
612
- if RUBY_VERSION.start_with?("2.7")
613
- using Module.new {
614
- refine UrlHelper do
615
- alias :_current_page? :current_page?
616
- end
617
- }
618
-
619
- def current_page?(*args) # :nodoc:
620
- options = args.pop
621
- options.is_a?(Hash) ? _current_page?(*args, **options) : _current_page?(*args, options)
622
- end
623
- end
624
-
625
580
  # Creates an SMS anchor link tag to the specified +phone_number+. When the
626
581
  # link is clicked, the default SMS messaging app is opened ready to send a
627
582
  # message to the linked phone number. If the +body+ option is specified,
@@ -757,7 +712,7 @@ module ActionView
757
712
  end
758
713
 
759
714
  def add_method_to_attributes!(html_options, method)
760
- if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
715
+ if method_not_get_method?(method) && !html_options["rel"].to_s.include?("nofollow")
761
716
  if html_options["rel"].blank?
762
717
  html_options["rel"] = "nofollow"
763
718
  else
@@ -12,6 +12,7 @@ require "action_view/helpers/asset_tag_helper"
12
12
  require "action_view/helpers/asset_url_helper"
13
13
  require "action_view/helpers/atom_feed_helper"
14
14
  require "action_view/helpers/cache_helper"
15
+ require "action_view/helpers/content_exfiltration_prevention_helper"
15
16
  require "action_view/helpers/controller_helper"
16
17
  require "action_view/helpers/csp_helper"
17
18
  require "action_view/helpers/csrf_helper"
@@ -45,6 +46,7 @@ module ActionView # :nodoc:
45
46
  include AtomFeedHelper
46
47
  include CacheHelper
47
48
  include CaptureHelper
49
+ include ContentExfiltrationPreventionHelper
48
50
  include ControllerHelper
49
51
  include CspHelper
50
52
  include CsrfHelper
@@ -4,12 +4,14 @@ require "action_view/rendering"
4
4
  require "active_support/core_ext/module/redefine_method"
5
5
 
6
6
  module ActionView
7
+ # = Action View \Layouts
8
+ #
7
9
  # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
8
10
  # repeated setups. The inclusion pattern has pages that look like this:
9
11
  #
10
- # <%= render "shared/header" %>
12
+ # <%= render "application/header" %>
11
13
  # Hello World
12
- # <%= render "shared/footer" %>
14
+ # <%= render "application/footer" %>
13
15
  #
14
16
  # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
15
17
  # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
@@ -150,7 +152,7 @@ module ActionView
150
152
  # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
151
153
  # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
152
154
  #
153
- # Setting the layout to +nil+ forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
155
+ # Setting the layout to +nil+ forces it to be looked up in the filesystem and falls back to the parent behavior if none exists.
154
156
  # Setting it to +nil+ is useful to re-enable template lookup overriding a previous configuration set in the parent:
155
157
  #
156
158
  # class ApplicationController < ActionController::Base
@@ -162,7 +164,7 @@ module ActionView
162
164
  # end
163
165
  #
164
166
  # class CommentsController < ApplicationController
165
- # # Will search for "comments" layout and fallback "application" layout
167
+ # # Will search for "comments" layout and fall back to "application" layout
166
168
  # layout nil
167
169
  # end
168
170
  #
@@ -207,13 +209,11 @@ module ActionView
207
209
 
208
210
  included do
209
211
  class_attribute :_layout, instance_accessor: false
210
- class_attribute :_layout_conditions, instance_accessor: false, default: {}
212
+ class_attribute :_layout_conditions, instance_accessor: false, instance_reader: true, default: {}
211
213
 
212
214
  _write_layout_method
213
215
  end
214
216
 
215
- delegate :_layout_conditions, to: :class
216
-
217
217
  module ClassMethods
218
218
  def inherited(klass) # :nodoc:
219
219
  super
@@ -428,7 +428,7 @@ module ActionView
428
428
  end
429
429
 
430
430
  def _include_layout?(options)
431
- (options.keys & [:body, :plain, :html, :inline, :partial]).empty? || options.key?(:layout)
431
+ !options.keys.intersect?([:body, :plain, :html, :inline, :partial]) || options.key?(:layout)
432
432
  end
433
433
  end
434
434
  end
@@ -5,7 +5,7 @@ require "active_support/log_subscriber"
5
5
  module ActionView
6
6
  # = Action View Log Subscriber
7
7
  #
8
- # Provides functionality so that Rails can output logs from Action View.
8
+ # Provides functionality so that \Rails can output logs from Action View.
9
9
  class LogSubscriber < ActiveSupport::LogSubscriber
10
10
  VIEWS_PATTERN = /^app\/views\//
11
11
 
@@ -18,26 +18,29 @@ 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
+ subscribe_log_level :render_template, :debug
24
25
 
25
26
  def render_partial(event)
26
27
  debug do
27
28
  message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
28
29
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
29
- 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)"
30
31
  message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
31
32
  message
32
33
  end
33
34
  end
35
+ subscribe_log_level :render_partial, :debug
34
36
 
35
37
  def render_layout(event)
36
38
  info do
37
39
  message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
38
- 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)"
39
41
  end
40
42
  end
43
+ subscribe_log_level :render_layout, :info
41
44
 
42
45
  def render_collection(event)
43
46
  identifier = event.payload[:identifier] || "templates"
@@ -45,33 +48,68 @@ module ActionView
45
48
  debug do
46
49
  message = +" Rendered collection of #{from_rails_root(identifier)}"
47
50
  message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
48
- 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)"
49
52
  message
50
53
  end
51
54
  end
55
+ subscribe_log_level :render_collection, :debug
52
56
 
53
- def start(name, id, payload)
54
- log_rendering_start(payload, name)
57
+ module Utils # :nodoc:
58
+ def logger
59
+ ActionView::Base.logger
60
+ end
55
61
 
56
- super
57
- end
62
+ private
63
+ def from_rails_root(string)
64
+ string = string.sub(rails_root, "")
65
+ string.sub!(VIEWS_PATTERN, "")
66
+ string
67
+ end
58
68
 
59
- def logger
60
- ActionView::Base.logger
69
+ def rails_root # :doc:
70
+ @root ||= "#{Rails.root}/"
71
+ end
61
72
  end
62
73
 
63
- private
64
- EMPTY = ""
65
- def from_rails_root(string) # :doc:
66
- string = string.sub(rails_root, EMPTY)
67
- string.sub!(VIEWS_PATTERN, EMPTY)
68
- string
74
+ include Utils
75
+
76
+ class Start # :nodoc:
77
+ include Utils
78
+
79
+ def start(name, id, payload)
80
+ return unless logger
81
+ logger.debug do
82
+ qualifier =
83
+ if name == "render_template.action_view"
84
+ ""
85
+ elsif name == "render_layout.action_view"
86
+ "layout "
87
+ end
88
+
89
+ return unless qualifier
90
+
91
+ message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
92
+ message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
93
+ message
94
+ end
95
+ end
96
+
97
+ def finish(name, id, payload)
98
+ end
99
+
100
+ def silenced?(_)
101
+ logger.nil? || !logger.debug?
102
+ end
69
103
  end
70
104
 
71
- def rails_root # :doc:
72
- @root ||= "#{Rails.root}/"
105
+ def self.attach_to(*)
106
+ ActiveSupport::Notifications.subscribe("render_template.action_view", ActionView::LogSubscriber::Start.new)
107
+ ActiveSupport::Notifications.subscribe("render_layout.action_view", ActionView::LogSubscriber::Start.new)
108
+
109
+ super
73
110
  end
74
111
 
112
+ private
75
113
  def render_count(payload) # :doc:
76
114
  if payload[:cache_hits]
77
115
  "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
@@ -88,23 +126,6 @@ module ActionView
88
126
  "[cache miss]"
89
127
  end
90
128
  end
91
-
92
- def log_rendering_start(payload, name)
93
- debug do
94
- qualifier =
95
- if name == "render_template.action_view"
96
- ""
97
- elsif name == "render_layout.action_view"
98
- "layout "
99
- end
100
-
101
- return unless qualifier
102
-
103
- message = +" Rendering #{qualifier}#{from_rails_root(payload[:identifier])}"
104
- message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
105
- message
106
- end
107
- end
108
129
  end
109
130
  end
110
131
 
@@ -13,7 +13,7 @@ module ActionView
13
13
  # view paths, used in the resolver cache lookup. Since this key is generated
14
14
  # only once during the request, it speeds up all cache accesses.
15
15
  class LookupContext # :nodoc:
16
- attr_accessor :prefixes, :rendered_format
16
+ attr_accessor :prefixes
17
17
 
18
18
  singleton_class.attr_accessor :registered_details
19
19
  self.registered_details = []
@@ -63,16 +63,20 @@ module ActionView
63
63
  end
64
64
 
65
65
  def self.details_cache_key(details)
66
- if details[:formats]
67
- details = details.dup
68
- details[:formats] &= Template::Types.symbols
66
+ @details_keys.fetch(details) do
67
+ if formats = details[:formats]
68
+ unless Template::Types.valid_symbols?(formats)
69
+ details = details.dup
70
+ details[:formats] &= Template::Types.symbols
71
+ end
72
+ end
73
+ @details_keys[details] ||= TemplateDetails::Requested.new(**details)
69
74
  end
70
- @details_keys[details] ||= TemplateDetails::Requested.new(**details)
71
75
  end
72
76
 
73
77
  def self.clear
74
- ActionView::ViewPaths.all_view_paths.each do |path_set|
75
- path_set.each(&:clear_cache)
78
+ ActionView::PathRegistry.all_resolvers.each do |resolver|
79
+ resolver.clear_cache
76
80
  end
77
81
  @view_context_class = nil
78
82
  @details_keys.clear
@@ -83,9 +87,9 @@ module ActionView
83
87
  @digest_cache.values
84
88
  end
85
89
 
86
- def self.view_context_class(klass)
90
+ def self.view_context_class
87
91
  @view_context_mutex.synchronize do
88
- @view_context_class ||= klass.with_empty_template_cache
92
+ @view_context_class ||= ActionView::Base.with_empty_template_cache
89
93
  end
90
94
  end
91
95
  end
@@ -148,11 +152,23 @@ module ActionView
148
152
  end
149
153
  alias :any_templates? :any?
150
154
 
155
+ def append_view_paths(paths)
156
+ @view_paths = build_view_paths(@view_paths.to_a + paths)
157
+ end
158
+
159
+ def prepend_view_paths(paths)
160
+ @view_paths = build_view_paths(paths + @view_paths.to_a)
161
+ end
162
+
151
163
  private
152
164
  # Whenever setting view paths, makes a copy so that we can manipulate them in
153
165
  # instance objects as we wish.
154
166
  def build_view_paths(paths)
155
- ActionView::PathSet.new(Array(paths))
167
+ if ActionView::PathSet === paths
168
+ paths
169
+ else
170
+ ActionView::PathSet.new(Array(paths))
171
+ end
156
172
  end
157
173
 
158
174
  # Compute details hash and key according to user options (e.g. passed from #render).
@@ -250,12 +266,12 @@ module ActionView
250
266
  values.concat(default_formats) if values.delete "*/*"
251
267
  values.uniq!
252
268
 
253
- invalid_values = (values - Template::Types.symbols)
254
- unless invalid_values.empty?
269
+ unless Template::Types.valid_symbols?(values)
270
+ invalid_values = values - Template::Types.symbols
255
271
  raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
256
272
  end
257
273
 
258
- if values == [:js]
274
+ if (values.length == 1) && (values[0] == :js)
259
275
  values << :html
260
276
  @html_fallback_for_js = true
261
277
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView # :nodoc:
4
+ module PathRegistry # :nodoc:
5
+ @view_paths_by_class = {}
6
+ @file_system_resolvers = {}
7
+ @file_system_resolver_mutex = Mutex.new
8
+ @file_system_resolver_hooks = []
9
+
10
+ class << self
11
+ attr_reader :file_system_resolver_hooks
12
+ end
13
+
14
+ def self.get_view_paths(klass)
15
+ @view_paths_by_class[klass] || get_view_paths(klass.superclass)
16
+ end
17
+
18
+ def self.set_view_paths(klass, paths)
19
+ @view_paths_by_class[klass] = paths
20
+ end
21
+
22
+ def self.cast_file_system_resolvers(paths)
23
+ paths = Array(paths)
24
+
25
+ @file_system_resolver_mutex.synchronize do
26
+ built_resolver = false
27
+ paths = paths.map do |path|
28
+ case path
29
+ when String, Pathname
30
+ path = File.expand_path(path)
31
+ @file_system_resolvers[path] ||=
32
+ begin
33
+ built_resolver = true
34
+ FileSystemResolver.new(path)
35
+ end
36
+ else
37
+ path
38
+ end
39
+ end
40
+
41
+ file_system_resolver_hooks.each(&:call) if built_resolver
42
+ end
43
+
44
+ paths
45
+ end
46
+
47
+ def self.all_resolvers
48
+ resolvers = [all_file_system_resolvers]
49
+ resolvers.concat @view_paths_by_class.values.map(&:to_a)
50
+ resolvers.flatten.uniq
51
+ end
52
+
53
+ def self.all_file_system_resolvers
54
+ @file_system_resolvers.values
55
+ end
56
+ end
57
+ end
@@ -13,14 +13,14 @@ module ActionView # :nodoc:
13
13
 
14
14
  attr_reader :paths
15
15
 
16
- delegate :[], :include?, :pop, :size, :each, to: :paths
16
+ delegate :[], :include?, :size, :each, to: :paths
17
17
 
18
18
  def initialize(paths = [])
19
- @paths = typecast paths
19
+ @paths = typecast(paths).freeze
20
20
  end
21
21
 
22
22
  def initialize_copy(other)
23
- @paths = other.paths.dup
23
+ @paths = other.paths.dup.freeze
24
24
  self
25
25
  end
26
26
 
@@ -32,18 +32,11 @@ module ActionView # :nodoc:
32
32
  PathSet.new paths.compact
33
33
  end
34
34
 
35
- def +(array)
35
+ def +(other)
36
+ array = Array === other ? other : other.paths
36
37
  PathSet.new(paths + array)
37
38
  end
38
39
 
39
- %w(<< concat push insert unshift).each do |method|
40
- class_eval <<-METHOD, __FILE__, __LINE__ + 1
41
- def #{method}(*args)
42
- paths.#{method}(*typecast(args))
43
- end
44
- METHOD
45
- end
46
-
47
40
  def find(path, prefixes, partial, details, details_key, locals)
48
41
  find_all(path, prefixes, partial, details, details_key, locals).first ||
49
42
  raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
@@ -75,9 +68,15 @@ module ActionView # :nodoc:
75
68
  paths.map do |path|
76
69
  case path
77
70
  when Pathname, String
78
- FileSystemResolver.new path.to_s
79
- else
71
+ # This path should only be reached by "direct" users of
72
+ # ActionView::Base (not using the ViewPaths or Renderer modules).
73
+ # We can't cache/de-dup the file system resolver in this case as we
74
+ # don't know which compiled_method_container we'll be rendering to.
75
+ FileSystemResolver.new(path)
76
+ when Resolver
80
77
  path
78
+ else
79
+ raise TypeError, "#{path.inspect} is not a valid path: must be a String, Pathname, or Resolver"
81
80
  end
82
81
  end
83
82
  end