actionview 5.1.4 → 6.1.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionview might be problematic. Click here for more details.

Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +199 -168
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -5
  5. data/lib/action_view.rb +10 -4
  6. data/lib/action_view/base.rb +87 -23
  7. data/lib/action_view/buffers.rb +17 -0
  8. data/lib/action_view/cache_expiry.rb +52 -0
  9. data/lib/action_view/context.rb +7 -11
  10. data/lib/action_view/dependency_tracker.rb +12 -4
  11. data/lib/action_view/digestor.rb +24 -23
  12. data/lib/action_view/flows.rb +2 -1
  13. data/lib/action_view/gem_version.rb +4 -2
  14. data/lib/action_view/helpers.rb +4 -2
  15. data/lib/action_view/helpers/active_model_helper.rb +9 -4
  16. data/lib/action_view/helpers/asset_tag_helper.rb +220 -57
  17. data/lib/action_view/helpers/asset_url_helper.rb +28 -23
  18. data/lib/action_view/helpers/atom_feed_helper.rb +5 -2
  19. data/lib/action_view/helpers/cache_helper.rb +39 -28
  20. data/lib/action_view/helpers/capture_helper.rb +13 -7
  21. data/lib/action_view/helpers/controller_helper.rb +3 -1
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +5 -3
  24. data/lib/action_view/helpers/date_helper.rb +78 -33
  25. data/lib/action_view/helpers/debug_helper.rb +4 -2
  26. data/lib/action_view/helpers/form_helper.rb +357 -106
  27. data/lib/action_view/helpers/form_options_helper.rb +45 -39
  28. data/lib/action_view/helpers/form_tag_helper.rb +42 -27
  29. data/lib/action_view/helpers/javascript_helper.rb +28 -12
  30. data/lib/action_view/helpers/number_helper.rb +16 -8
  31. data/lib/action_view/helpers/output_safety_helper.rb +3 -1
  32. data/lib/action_view/helpers/rendering_helper.rb +20 -9
  33. data/lib/action_view/helpers/sanitize_helper.rb +15 -19
  34. data/lib/action_view/helpers/tag_helper.rb +100 -24
  35. data/lib/action_view/helpers/tags.rb +3 -1
  36. data/lib/action_view/helpers/tags/base.rb +30 -21
  37. data/lib/action_view/helpers/tags/check_box.rb +3 -2
  38. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +2 -1
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +2 -1
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +2 -1
  42. data/lib/action_view/helpers/tags/collection_select.rb +3 -1
  43. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  44. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/date_select.rb +5 -4
  46. data/lib/action_view/helpers/tags/datetime_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  48. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -1
  52. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/label.rb +6 -5
  54. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  55. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +2 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +3 -2
  59. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +2 -0
  61. data/lib/action_view/helpers/tags/select.rb +4 -3
  62. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +3 -1
  64. data/lib/action_view/helpers/tags/text_field.rb +3 -2
  65. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  66. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  68. data/lib/action_view/helpers/tags/translator.rb +3 -6
  69. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  71. data/lib/action_view/helpers/text_helper.rb +11 -10
  72. data/lib/action_view/helpers/translation_helper.rb +102 -52
  73. data/lib/action_view/helpers/url_helper.rb +150 -32
  74. data/lib/action_view/layouts.rb +15 -15
  75. data/lib/action_view/log_subscriber.rb +32 -15
  76. data/lib/action_view/lookup_context.rb +67 -39
  77. data/lib/action_view/model_naming.rb +2 -0
  78. data/lib/action_view/path_set.rb +5 -12
  79. data/lib/action_view/railtie.rb +46 -21
  80. data/lib/action_view/record_identifier.rb +4 -3
  81. data/lib/action_view/renderer/abstract_renderer.rb +144 -11
  82. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  83. data/lib/action_view/renderer/object_renderer.rb +34 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +33 -283
  85. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +64 -17
  86. data/lib/action_view/renderer/renderer.rb +61 -4
  87. data/lib/action_view/renderer/streaming_template_renderer.rb +14 -8
  88. data/lib/action_view/renderer/template_renderer.rb +36 -26
  89. data/lib/action_view/rendering.rb +57 -38
  90. data/lib/action_view/routing_url_for.rb +15 -12
  91. data/lib/action_view/tasks/cache_digests.rake +2 -0
  92. data/lib/action_view/template.rb +69 -76
  93. data/lib/action_view/template/error.rb +32 -18
  94. data/lib/action_view/template/handlers.rb +4 -2
  95. data/lib/action_view/template/handlers/builder.rb +5 -6
  96. data/lib/action_view/template/handlers/erb.rb +20 -19
  97. data/lib/action_view/template/handlers/erb/erubi.rb +17 -9
  98. data/lib/action_view/template/handlers/html.rb +3 -1
  99. data/lib/action_view/template/handlers/raw.rb +4 -2
  100. data/lib/action_view/template/html.rb +8 -7
  101. data/lib/action_view/template/inline.rb +22 -0
  102. data/lib/action_view/template/raw_file.rb +25 -0
  103. data/lib/action_view/template/renderable.rb +24 -0
  104. data/lib/action_view/template/resolver.rb +194 -152
  105. data/lib/action_view/template/sources.rb +13 -0
  106. data/lib/action_view/template/sources/file.rb +17 -0
  107. data/lib/action_view/template/text.rb +5 -4
  108. data/lib/action_view/template/types.rb +3 -1
  109. data/lib/action_view/test_case.rb +38 -30
  110. data/lib/action_view/testing/resolvers.rb +20 -27
  111. data/lib/action_view/unbound_template.rb +31 -0
  112. data/lib/action_view/version.rb +2 -0
  113. data/lib/action_view/view_paths.rb +61 -40
  114. data/lib/assets/compiled/rails-ujs.js +84 -23
  115. metadata +34 -23
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -21
  117. data/lib/action_view/template/handlers/erb/deprecated_erubis.rb +0 -9
  118. data/lib/action_view/template/handlers/erb/erubis.rb +0 -81
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "concurrent/map"
2
- require "active_support/core_ext/module/remove_method"
3
4
  require "active_support/core_ext/module/attribute_accessors"
4
5
  require "action_view/template/resolver"
5
6
 
@@ -14,20 +15,18 @@ module ActionView
14
15
  class LookupContext #:nodoc:
15
16
  attr_accessor :prefixes, :rendered_format
16
17
 
17
- mattr_accessor :fallbacks
18
- @@fallbacks = FallbackFileSystemResolver.instances
18
+ mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances
19
19
 
20
- mattr_accessor :registered_details
21
- self.registered_details = []
20
+ mattr_accessor :registered_details, default: []
22
21
 
23
22
  def self.register_detail(name, &block)
24
23
  registered_details << name
25
24
  Accessors::DEFAULT_PROCS[name] = block
26
25
 
27
- Accessors.send :define_method, :"default_#{name}", &block
26
+ Accessors.define_method(:"default_#{name}", &block)
28
27
  Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
29
28
  def #{name}
30
- @details.fetch(:#{name}, [])
29
+ @details[:#{name}] || []
31
30
  end
32
31
 
33
32
  def #{name}=(value)
@@ -57,21 +56,39 @@ module ActionView
57
56
  alias :eql? :equal?
58
57
 
59
58
  @details_keys = Concurrent::Map.new
59
+ @digest_cache = Concurrent::Map.new
60
+ @view_context_mutex = Mutex.new
61
+
62
+ def self.digest_cache(details)
63
+ @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new
64
+ end
60
65
 
61
- def self.get(details)
66
+ def self.details_cache_key(details)
62
67
  if details[:formats]
63
68
  details = details.dup
64
69
  details[:formats] &= Template::Types.symbols
65
70
  end
66
- @details_keys[details] ||= Concurrent::Map.new
71
+ @details_keys[details] ||= Object.new
67
72
  end
68
73
 
69
74
  def self.clear
75
+ ActionView::ViewPaths.all_view_paths.each do |path_set|
76
+ path_set.each(&:clear_cache)
77
+ end
78
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
79
+ @view_context_class = nil
70
80
  @details_keys.clear
81
+ @digest_cache.clear
71
82
  end
72
83
 
73
84
  def self.digest_caches
74
- @details_keys.values
85
+ @digest_cache.values
86
+ end
87
+
88
+ def self.view_context_class(klass)
89
+ @view_context_mutex.synchronize do
90
+ @view_context_class ||= klass.with_empty_template_cache
91
+ end
75
92
  end
76
93
  end
77
94
 
@@ -82,7 +99,7 @@ module ActionView
82
99
  # Calculate the details key. Remove the handlers from calculation to improve performance
83
100
  # since the user cannot modify it explicitly.
84
101
  def details_key #:nodoc:
85
- @details_key ||= DetailsKey.get(@details) if @cache
102
+ @details_key ||= DetailsKey.details_cache_key(@details) if @cache
86
103
  end
87
104
 
88
105
  # Temporary skip passing the details_key forward.
@@ -94,9 +111,9 @@ module ActionView
94
111
  end
95
112
 
96
113
  private
97
-
98
114
  def _set_detail(key, value) # :doc:
99
- @details = @details.dup if @details_key
115
+ @details = @details.dup if @digest_cache || @details_key
116
+ @digest_cache = nil
100
117
  @details_key = nil
101
118
  @details[key] = value
102
119
  end
@@ -106,21 +123,11 @@ module ActionView
106
123
  module ViewPaths
107
124
  attr_reader :view_paths, :html_fallback_for_js
108
125
 
109
- # Whenever setting view paths, makes a copy so that we can manipulate them in
110
- # instance objects as we wish.
111
- def view_paths=(paths)
112
- @view_paths = ActionView::PathSet.new(Array(paths))
113
- end
114
-
115
126
  def find(name, prefixes = [], partial = false, keys = [], options = {})
116
127
  @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
117
128
  end
118
129
  alias :find_template :find
119
130
 
120
- def find_file(name, prefixes = [], partial = false, keys = [], options = {})
121
- @view_paths.find_file(*args_for_lookup(name, prefixes, partial, keys, options))
122
- end
123
-
124
131
  def find_all(name, prefixes = [], partial = false, keys = [], options = {})
125
132
  @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
126
133
  end
@@ -138,18 +145,24 @@ module ActionView
138
145
  # Adds fallbacks to the view paths. Useful in cases when you are rendering
139
146
  # a :file.
140
147
  def with_fallbacks
141
- added_resolvers = 0
142
- self.class.fallbacks.each do |resolver|
143
- next if view_paths.include?(resolver)
144
- view_paths.push(resolver)
145
- added_resolvers += 1
148
+ view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
149
+
150
+ if block_given?
151
+ raise ArgumentError, <<~eowarn.squish
152
+ Calling `with_fallbacks` with a block is not supported. Call methods on
153
+ the lookup context returned by `with_fallbacks` instead.
154
+ eowarn
155
+ else
156
+ ActionView::LookupContext.new(view_paths, @details, @prefixes)
146
157
  end
147
- yield
148
- ensure
149
- added_resolvers.times { view_paths.pop }
150
158
  end
151
159
 
152
160
  private
161
+ # Whenever setting view paths, makes a copy so that we can manipulate them in
162
+ # instance objects as we wish.
163
+ def build_view_paths(paths)
164
+ ActionView::PathSet.new(Array(paths))
165
+ end
153
166
 
154
167
  def args_for_lookup(name, prefixes, partial, keys, details_options)
155
168
  name, prefixes = normalize_name(name, prefixes)
@@ -163,7 +176,7 @@ module ActionView
163
176
  user_details = @details.merge(options)
164
177
 
165
178
  if @cache
166
- details_key = DetailsKey.get(user_details)
179
+ details_key = DetailsKey.details_cache_key(user_details)
167
180
  else
168
181
  details_key = nil
169
182
  end
@@ -190,7 +203,7 @@ module ActionView
190
203
  end
191
204
 
192
205
  if @cache
193
- [details, DetailsKey.get(details)]
206
+ [details, DetailsKey.details_cache_key(details)]
194
207
  else
195
208
  [details, nil]
196
209
  end
@@ -202,13 +215,13 @@ module ActionView
202
215
  # name instead of the prefix.
203
216
  def normalize_name(name, prefixes)
204
217
  prefixes = prefixes.presence
205
- parts = name.to_s.split("/".freeze)
218
+ parts = name.to_s.split("/")
206
219
  parts.shift if parts.first.empty?
207
220
  name = parts.pop
208
221
 
209
222
  return name, prefixes || [""] if parts.empty?
210
223
 
211
- parts = parts.join("/".freeze)
224
+ parts = parts.join("/")
212
225
  prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
213
226
 
214
227
  return name, prefixes
@@ -221,16 +234,23 @@ module ActionView
221
234
 
222
235
  def initialize(view_paths, details = {}, prefixes = [])
223
236
  @details_key = nil
237
+ @digest_cache = nil
224
238
  @cache = true
225
239
  @prefixes = prefixes
226
- @rendered_format = nil
227
240
 
228
241
  @details = initialize_details({}, details)
229
- self.view_paths = view_paths
242
+ @view_paths = build_view_paths(view_paths)
230
243
  end
231
244
 
232
245
  def digest_cache
233
- details_key
246
+ @digest_cache ||= DetailsKey.digest_cache(@details)
247
+ end
248
+
249
+ def with_prepended_formats(formats)
250
+ details = @details.dup
251
+ details[:formats] = formats
252
+
253
+ self.class.new(@view_paths, details, @prefixes)
234
254
  end
235
255
 
236
256
  def initialize_details(target, details)
@@ -245,7 +265,15 @@ module ActionView
245
265
  # add :html as fallback to :js.
246
266
  def formats=(values)
247
267
  if values
248
- values.concat(default_formats) if values.delete "*/*".freeze
268
+ values = values.dup
269
+ values.concat(default_formats) if values.delete "*/*"
270
+ values.uniq!
271
+
272
+ invalid_values = (values - Template::Types.symbols)
273
+ unless invalid_values.empty?
274
+ raise ArgumentError, "Invalid formats: #{invalid_values.map(&:inspect).join(", ")}"
275
+ end
276
+
249
277
  if values == [:js]
250
278
  values << :html
251
279
  @html_fallback_for_js = true
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView
2
4
  module ModelNaming #:nodoc:
3
5
  # Converts the given object to an ActiveModel compliant one.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActionView #:nodoc:
2
4
  # = Action View PathSet
3
5
  #
@@ -46,12 +48,8 @@ module ActionView #:nodoc:
46
48
  find_all(*args).first || raise(MissingTemplate.new(self, *args))
47
49
  end
48
50
 
49
- def find_file(path, prefixes = [], *args)
50
- _find_all(path, prefixes, args, true).first || raise(MissingTemplate.new(self, path, prefixes, *args))
51
- end
52
-
53
51
  def find_all(path, prefixes = [], *args)
54
- _find_all path, prefixes, args, false
52
+ _find_all path, prefixes, args
55
53
  end
56
54
 
57
55
  def exists?(path, prefixes, *args)
@@ -68,16 +66,11 @@ module ActionView #:nodoc:
68
66
  end
69
67
 
70
68
  private
71
-
72
- def _find_all(path, prefixes, args, outside_app)
69
+ def _find_all(path, prefixes, args)
73
70
  prefixes = [prefixes] if String === prefixes
74
71
  prefixes.each do |prefix|
75
72
  paths.each do |resolver|
76
- if outside_app
77
- templates = resolver.find_all_anywhere(path, prefix, *args)
78
- else
79
- templates = resolver.find_all(path, prefix, *args)
80
- end
73
+ templates = resolver.find_all(path, prefix, *args)
81
74
  return templates unless templates.empty?
82
75
  end
83
76
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "action_view"
2
4
  require "rails"
3
5
 
@@ -7,37 +9,56 @@ module ActionView
7
9
  config.action_view = ActiveSupport::OrderedOptions.new
8
10
  config.action_view.embed_authenticity_token_in_remote_forms = nil
9
11
  config.action_view.debug_missing_translation = true
12
+ config.action_view.default_enforce_utf8 = nil
10
13
 
11
14
  config.eager_load_namespaces << ActionView
12
15
 
13
- initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
14
- ActiveSupport.on_load(:action_view) do
15
- ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
16
- app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
16
+ config.after_initialize do |app|
17
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
18
+ app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
19
+ end
20
+
21
+ config.after_initialize do |app|
22
+ form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
23
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
24
+ end
25
+
26
+ config.after_initialize do |app|
27
+ form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
28
+ unless form_with_generates_ids.nil?
29
+ ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
17
30
  end
18
31
  end
19
32
 
20
- initializer "action_view.form_with_generates_remote_forms" do |app|
21
- ActiveSupport.on_load(:action_view) do
22
- form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
23
- unless form_with_generates_remote_forms.nil?
24
- ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
25
- end
33
+ config.after_initialize do |app|
34
+ default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
35
+ unless default_enforce_utf8.nil?
36
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
26
37
  end
27
38
  end
28
39
 
29
- initializer "action_view.logger" do
30
- ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
40
+ config.after_initialize do |app|
41
+ ActionView::Helpers::AssetTagHelper.preload_links_header = app.config.action_view.delete(:preload_links_header)
31
42
  end
32
43
 
33
- initializer "action_view.set_configs" do |app|
44
+ config.after_initialize do |app|
34
45
  ActiveSupport.on_load(:action_view) do
35
46
  app.config.action_view.each do |k, v|
47
+ if k == :raise_on_missing_translations
48
+ ActiveSupport::Deprecation.warn \
49
+ "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 6.2. " \
50
+ "Set i18n.raise_on_missing_translations instead. " \
51
+ "Note that this new setting also affects how missing translations are handled in controllers."
52
+ end
36
53
  send "#{k}=", v
37
54
  end
38
55
  end
39
56
  end
40
57
 
58
+ initializer "action_view.logger" do
59
+ ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
60
+ end
61
+
41
62
  initializer "action_view.caching" do |app|
42
63
  ActiveSupport.on_load(:action_view) do
43
64
  if app.config.action_view.cache_template_loading.nil?
@@ -46,14 +67,6 @@ module ActionView
46
67
  end
47
68
  end
48
69
 
49
- initializer "action_view.per_request_digest_cache" do |app|
50
- ActiveSupport.on_load(:action_view) do
51
- unless ActionView::Resolver.caching?
52
- app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
53
- end
54
- end
55
- end
56
-
57
70
  initializer "action_view.setup_action_pack" do |app|
58
71
  ActiveSupport.on_load(:action_controller) do
59
72
  ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
@@ -64,6 +77,18 @@ module ActionView
64
77
  PartialRenderer.collection_cache = app.config.action_controller.cache_store
65
78
  end
66
79
 
80
+ config.after_initialize do |app|
81
+ enable_caching = if app.config.action_view.cache_template_loading.nil?
82
+ app.config.cache_classes
83
+ else
84
+ app.config.action_view.cache_template_loading
85
+ end
86
+
87
+ unless enable_caching
88
+ app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
89
+ end
90
+ end
91
+
67
92
  rake_tasks do |app|
68
93
  unless app.config.api_only
69
94
  load "action_view/tasks/cache_digests.rake"
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_support/core_ext/module"
2
4
  require "action_view/model_naming"
3
5
 
@@ -57,8 +59,8 @@ module ActionView
57
59
 
58
60
  include ModelNaming
59
61
 
60
- JOIN = "_".freeze
61
- NEW = "new".freeze
62
+ JOIN = "_"
63
+ NEW = "new"
62
64
 
63
65
  # The DOM class convention is to use the singular form of an object or class.
64
66
  #
@@ -93,7 +95,6 @@ module ActionView
93
95
  end
94
96
 
95
97
  private
96
-
97
98
  # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
98
99
  # This can be overwritten to customize the default generated string representation if desired.
99
100
  # If you need to read back a key from a dom_id in order to query for the underlying database record,
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
1
5
  module ActionView
2
6
  # This class defines the interface for a renderer. Each class that
3
7
  # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
@@ -12,10 +16,10 @@ module ActionView
12
16
  #
13
17
  # Whenever the +render+ method is called on the base +Renderer+ class, a new
14
18
  # renderer object of the correct type is created, and the +render+ method on
15
- # that new object is called in turn. This abstracts the setup and rendering
19
+ # that new object is called in turn. This abstracts the set up and rendering
16
20
  # into a separate classes for partials and templates.
17
21
  class AbstractRenderer #:nodoc:
18
- delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
22
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
19
23
 
20
24
  def initialize(lookup_context)
21
25
  @lookup_context = lookup_context
@@ -25,22 +29,143 @@ module ActionView
25
29
  raise NotImplementedError
26
30
  end
27
31
 
28
- private
32
+ module ObjectRendering # :nodoc:
33
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
+ h[k] = Concurrent::Map.new
35
+ end
29
36
 
30
- def extract_details(options) # :doc:
31
- @lookup_context.registered_details.each_with_object({}) do |key, details|
32
- value = options[key]
37
+ def initialize(lookup_context, options)
38
+ super
39
+ @context_prefix = lookup_context.prefixes.first
40
+ end
41
+
42
+ private
43
+ def local_variable(path)
44
+ if as = @options[:as]
45
+ raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
46
+ as.to_sym
47
+ else
48
+ base = path.end_with?("/") ? "" : File.basename(path)
49
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
50
+ $1.to_sym
51
+ end
52
+ end
53
+
54
+ IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
55
+ "make sure your partial name starts with underscore."
56
+
57
+ OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
58
+ "make sure it starts with lowercase letter, " \
59
+ "and is followed by any combination of letters, numbers and underscores."
60
+
61
+ def raise_invalid_identifier(path)
62
+ raise ArgumentError, IDENTIFIER_ERROR_MESSAGE % path
63
+ end
33
64
 
34
- details[key] = Array(value) if value
65
+ def raise_invalid_option_as(as)
66
+ raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
35
67
  end
68
+
69
+ # Obtains the path to where the object's partial is located. If the object
70
+ # responds to +to_partial_path+, then +to_partial_path+ will be called and
71
+ # will provide the path. If the object does not respond to +to_partial_path+,
72
+ # then an +ArgumentError+ is raised.
73
+ #
74
+ # If +prefix_partial_path_with_controller_namespace+ is true, then this
75
+ # method will prefix the partial paths with a namespace.
76
+ def partial_path(object, view)
77
+ object = object.to_model if object.respond_to?(:to_model)
78
+
79
+ path = if object.respond_to?(:to_partial_path)
80
+ object.to_partial_path
81
+ else
82
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
83
+ end
84
+
85
+ if view.prefix_partial_path_with_controller_namespace
86
+ PREFIXED_PARTIAL_NAMES[@context_prefix][path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
87
+ else
88
+ path
89
+ end
90
+ end
91
+
92
+ def merge_prefix_into_object_path(prefix, object_path)
93
+ if prefix.include?(?/) && object_path.include?(?/)
94
+ prefixes = []
95
+ prefix_array = File.dirname(prefix).split("/")
96
+ object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
97
+
98
+ prefix_array.each_with_index do |dir, index|
99
+ break if dir == object_path_array[index]
100
+ prefixes << dir
101
+ end
102
+
103
+ (prefixes << object_path).join("/")
104
+ else
105
+ object_path
106
+ end
107
+ end
108
+ end
109
+
110
+ class RenderedCollection # :nodoc:
111
+ def self.empty(format)
112
+ EmptyCollection.new format
36
113
  end
37
114
 
38
- def instrument(name, **options) # :doc:
39
- options[:identifier] ||= (@template && @template.identifier) || @path
115
+ attr_reader :rendered_templates
40
116
 
41
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
42
- yield payload
117
+ def initialize(rendered_templates, spacer)
118
+ @rendered_templates = rendered_templates
119
+ @spacer = spacer
120
+ end
121
+
122
+ def body
123
+ @rendered_templates.map(&:body).join(@spacer.body).html_safe
124
+ end
125
+
126
+ def format
127
+ rendered_templates.first.format
128
+ end
129
+
130
+ class EmptyCollection
131
+ attr_reader :format
132
+
133
+ def initialize(format)
134
+ @format = format
43
135
  end
136
+
137
+ def body; nil; end
138
+ end
139
+ end
140
+
141
+ class RenderedTemplate # :nodoc:
142
+ attr_reader :body, :template
143
+
144
+ def initialize(body, template)
145
+ @body = body
146
+ @template = template
147
+ end
148
+
149
+ def format
150
+ template.format
151
+ end
152
+
153
+ EMPTY_SPACER = Struct.new(:body).new
154
+ end
155
+
156
+ private
157
+ NO_DETAILS = {}.freeze
158
+
159
+ def extract_details(options) # :doc:
160
+ details = nil
161
+ @lookup_context.registered_details.each do |key|
162
+ value = options[key]
163
+
164
+ if value
165
+ (details ||= {})[key] = Array(value)
166
+ end
167
+ end
168
+ details || NO_DETAILS
44
169
  end
45
170
 
46
171
  def prepend_formats(formats) # :doc:
@@ -49,5 +174,13 @@ module ActionView
49
174
 
50
175
  @lookup_context.formats = formats | @lookup_context.formats
51
176
  end
177
+
178
+ def build_rendered_template(content, template)
179
+ RenderedTemplate.new content, template
180
+ end
181
+
182
+ def build_rendered_collection(templates, spacer)
183
+ RenderedCollection.new templates, spacer
184
+ end
52
185
  end
53
186
  end