actionview 5.2.4.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +221 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +5 -3
  5. data/lib/action_view.rb +7 -2
  6. data/lib/action_view/base.rb +81 -15
  7. data/lib/action_view/buffers.rb +15 -0
  8. data/lib/action_view/cache_expiry.rb +52 -0
  9. data/lib/action_view/context.rb +5 -9
  10. data/lib/action_view/dependency_tracker.rb +10 -4
  11. data/lib/action_view/digestor.rb +15 -22
  12. data/lib/action_view/flows.rb +0 -1
  13. data/lib/action_view/gem_version.rb +4 -4
  14. data/lib/action_view/helpers.rb +0 -2
  15. data/lib/action_view/helpers/active_model_helper.rb +0 -1
  16. data/lib/action_view/helpers/asset_tag_helper.rb +63 -46
  17. data/lib/action_view/helpers/asset_url_helper.rb +9 -6
  18. data/lib/action_view/helpers/atom_feed_helper.rb +2 -1
  19. data/lib/action_view/helpers/cache_helper.rb +23 -22
  20. data/lib/action_view/helpers/capture_helper.rb +4 -0
  21. data/lib/action_view/helpers/csp_helper.rb +4 -2
  22. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  23. data/lib/action_view/helpers/date_helper.rb +73 -30
  24. data/lib/action_view/helpers/form_helper.rb +305 -37
  25. data/lib/action_view/helpers/form_options_helper.rb +23 -23
  26. data/lib/action_view/helpers/form_tag_helper.rb +19 -16
  27. data/lib/action_view/helpers/javascript_helper.rb +12 -11
  28. data/lib/action_view/helpers/number_helper.rb +14 -8
  29. data/lib/action_view/helpers/output_safety_helper.rb +1 -1
  30. data/lib/action_view/helpers/rendering_helper.rb +17 -7
  31. data/lib/action_view/helpers/sanitize_helper.rb +12 -18
  32. data/lib/action_view/helpers/tag_helper.rb +98 -22
  33. data/lib/action_view/helpers/tags/base.rb +18 -11
  34. data/lib/action_view/helpers/tags/check_box.rb +0 -1
  35. data/lib/action_view/helpers/tags/collection_check_boxes.rb +0 -1
  36. data/lib/action_view/helpers/tags/collection_helpers.rb +0 -1
  37. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +0 -1
  38. data/lib/action_view/helpers/tags/color_field.rb +1 -2
  39. data/lib/action_view/helpers/tags/date_field.rb +1 -2
  40. data/lib/action_view/helpers/tags/date_select.rb +2 -3
  41. data/lib/action_view/helpers/tags/datetime_field.rb +0 -1
  42. data/lib/action_view/helpers/tags/datetime_local_field.rb +1 -2
  43. data/lib/action_view/helpers/tags/label.rb +4 -1
  44. data/lib/action_view/helpers/tags/month_field.rb +1 -2
  45. data/lib/action_view/helpers/tags/radio_button.rb +0 -1
  46. data/lib/action_view/helpers/tags/select.rb +1 -2
  47. data/lib/action_view/helpers/tags/text_field.rb +0 -1
  48. data/lib/action_view/helpers/tags/time_field.rb +1 -2
  49. data/lib/action_view/helpers/tags/translator.rb +1 -6
  50. data/lib/action_view/helpers/tags/week_field.rb +1 -2
  51. data/lib/action_view/helpers/text_helper.rb +3 -4
  52. data/lib/action_view/helpers/translation_helper.rb +93 -55
  53. data/lib/action_view/helpers/url_helper.rb +121 -27
  54. data/lib/action_view/layouts.rb +8 -10
  55. data/lib/action_view/log_subscriber.rb +30 -15
  56. data/lib/action_view/lookup_context.rb +63 -35
  57. data/lib/action_view/path_set.rb +3 -12
  58. data/lib/action_view/railtie.rb +42 -26
  59. data/lib/action_view/record_identifier.rb +2 -3
  60. data/lib/action_view/renderer/abstract_renderer.rb +142 -11
  61. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  62. data/lib/action_view/renderer/object_renderer.rb +34 -0
  63. data/lib/action_view/renderer/partial_renderer.rb +21 -273
  64. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +61 -16
  65. data/lib/action_view/renderer/renderer.rb +59 -4
  66. data/lib/action_view/renderer/streaming_template_renderer.rb +10 -8
  67. data/lib/action_view/renderer/template_renderer.rb +35 -27
  68. data/lib/action_view/rendering.rb +54 -33
  69. data/lib/action_view/routing_url_for.rb +13 -12
  70. data/lib/action_view/template.rb +66 -75
  71. data/lib/action_view/template/error.rb +30 -15
  72. data/lib/action_view/template/handlers.rb +1 -1
  73. data/lib/action_view/template/handlers/builder.rb +2 -2
  74. data/lib/action_view/template/handlers/erb.rb +16 -11
  75. data/lib/action_view/template/handlers/erb/erubi.rb +15 -9
  76. data/lib/action_view/template/handlers/html.rb +1 -1
  77. data/lib/action_view/template/handlers/raw.rb +2 -2
  78. data/lib/action_view/template/html.rb +5 -6
  79. data/lib/action_view/template/inline.rb +22 -0
  80. data/lib/action_view/template/raw_file.rb +25 -0
  81. data/lib/action_view/template/renderable.rb +24 -0
  82. data/lib/action_view/template/resolver.rb +191 -150
  83. data/lib/action_view/template/sources.rb +13 -0
  84. data/lib/action_view/template/sources/file.rb +17 -0
  85. data/lib/action_view/template/text.rb +2 -3
  86. data/lib/action_view/test_case.rb +21 -29
  87. data/lib/action_view/testing/resolvers.rb +18 -27
  88. data/lib/action_view/unbound_template.rb +31 -0
  89. data/lib/action_view/view_paths.rb +59 -38
  90. data/lib/assets/compiled/rails-ujs.js +29 -3
  91. metadata +32 -21
  92. data/lib/action_view/helpers/record_tag_helper.rb +0 -23
@@ -59,8 +59,8 @@ module ActionView
59
59
 
60
60
  include ModelNaming
61
61
 
62
- JOIN = "_".freeze
63
- NEW = "new".freeze
62
+ JOIN = "_"
63
+ NEW = "new"
64
64
 
65
65
  # The DOM class convention is to use the singular form of an object or class.
66
66
  #
@@ -95,7 +95,6 @@ module ActionView
95
95
  end
96
96
 
97
97
  private
98
-
99
98
  # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
100
99
  # This can be overwritten to customize the default generated string representation if desired.
101
100
  # If you need to read back a key from a dom_id in order to query for the underlying database record,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
4
+
3
5
  module ActionView
4
6
  # This class defines the interface for a renderer. Each class that
5
7
  # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
@@ -14,10 +16,10 @@ module ActionView
14
16
  #
15
17
  # Whenever the +render+ method is called on the base +Renderer+ class, a new
16
18
  # renderer object of the correct type is created, and the +render+ method on
17
- # 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
18
20
  # into a separate classes for partials and templates.
19
21
  class AbstractRenderer #:nodoc:
20
- 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
21
23
 
22
24
  def initialize(lookup_context)
23
25
  @lookup_context = lookup_context
@@ -27,22 +29,143 @@ module ActionView
27
29
  raise NotImplementedError
28
30
  end
29
31
 
30
- private
32
+ module ObjectRendering # :nodoc:
33
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
+ h[k] = Concurrent::Map.new
35
+ end
31
36
 
32
- def extract_details(options) # :doc:
33
- @lookup_context.registered_details.each_with_object({}) do |key, details|
34
- 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
64
+
65
+ def raise_invalid_option_as(as)
66
+ raise ArgumentError, OPTION_AS_ERROR_MESSAGE % as
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
113
+ end
35
114
 
36
- details[key] = Array(value) if value
115
+ attr_reader :rendered_templates
116
+
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
37
135
  end
136
+
137
+ def body; nil; end
38
138
  end
139
+ end
39
140
 
40
- def instrument(name, **options) # :doc:
41
- options[:identifier] ||= (@template && @template.identifier) || @path
141
+ class RenderedTemplate # :nodoc:
142
+ attr_reader :body, :template
42
143
 
43
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
44
- yield payload
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
45
167
  end
168
+ details || NO_DETAILS
46
169
  end
47
170
 
48
171
  def prepend_formats(formats) # :doc:
@@ -51,5 +174,13 @@ module ActionView
51
174
 
52
175
  @lookup_context.formats = formats | @lookup_context.formats
53
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
54
185
  end
55
186
  end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/renderer/partial_renderer"
4
+
5
+ module ActionView
6
+ class PartialIteration
7
+ # The number of iterations that will be done by the partial.
8
+ attr_reader :size
9
+
10
+ # The current iteration of the partial.
11
+ attr_reader :index
12
+
13
+ def initialize(size)
14
+ @size = size
15
+ @index = 0
16
+ end
17
+
18
+ # Check if this is the first iteration of the partial.
19
+ def first?
20
+ index == 0
21
+ end
22
+
23
+ # Check if this is the last iteration of the partial.
24
+ def last?
25
+ index == size - 1
26
+ end
27
+
28
+ def iterate! # :nodoc:
29
+ @index += 1
30
+ end
31
+ end
32
+
33
+ class CollectionRenderer < PartialRenderer # :nodoc:
34
+ include ObjectRendering
35
+
36
+ class CollectionIterator # :nodoc:
37
+ include Enumerable
38
+
39
+ def initialize(collection)
40
+ @collection = collection
41
+ end
42
+
43
+ def each(&blk)
44
+ @collection.each(&blk)
45
+ end
46
+
47
+ def size
48
+ @collection.size
49
+ end
50
+
51
+ def length
52
+ @collection.respond_to?(:length) ? @collection.length : size
53
+ end
54
+ end
55
+
56
+ class SameCollectionIterator < CollectionIterator # :nodoc:
57
+ def initialize(collection, path, variables)
58
+ super(collection)
59
+ @path = path
60
+ @variables = variables
61
+ end
62
+
63
+ def from_collection(collection)
64
+ self.class.new(collection, @path, @variables)
65
+ end
66
+
67
+ def each_with_info
68
+ return enum_for(:each_with_info) unless block_given?
69
+ variables = [@path] + @variables
70
+ @collection.each { |o| yield(o, variables) }
71
+ end
72
+ end
73
+
74
+ class PreloadCollectionIterator < SameCollectionIterator # :nodoc:
75
+ def initialize(collection, path, variables, relation)
76
+ super(collection, path, variables)
77
+ relation.skip_preloading! unless relation.loaded?
78
+ @relation = relation
79
+ end
80
+
81
+ def from_collection(collection)
82
+ self.class.new(collection, @path, @variables, @relation)
83
+ end
84
+
85
+ def each_with_info
86
+ return super unless block_given?
87
+ @relation.preload_associations(@collection)
88
+ super
89
+ end
90
+ end
91
+
92
+ class MixedCollectionIterator < CollectionIterator # :nodoc:
93
+ def initialize(collection, paths)
94
+ super(collection)
95
+ @paths = paths
96
+ end
97
+
98
+ def each_with_info
99
+ return enum_for(:each_with_info) unless block_given?
100
+ @collection.each_with_index { |o, i| yield(o, @paths[i]) }
101
+ end
102
+ end
103
+
104
+ def render_collection_with_partial(collection, partial, context, block)
105
+ iter_vars = retrieve_variable(partial)
106
+
107
+ collection = if collection.respond_to?(:preload_associations)
108
+ PreloadCollectionIterator.new(collection, partial, iter_vars, collection)
109
+ else
110
+ SameCollectionIterator.new(collection, partial, iter_vars)
111
+ end
112
+
113
+ template = find_template(partial, @locals.keys + iter_vars)
114
+
115
+ layout = if !block && (layout = @options[:layout])
116
+ find_template(layout.to_s, @locals.keys + iter_vars)
117
+ end
118
+
119
+ render_collection(collection, context, partial, template, layout, block)
120
+ end
121
+
122
+ def render_collection_derive_partial(collection, context, block)
123
+ paths = collection.map { |o| partial_path(o, context) }
124
+
125
+ if paths.uniq.length == 1
126
+ # Homogeneous
127
+ render_collection_with_partial(collection, paths.first, context, block)
128
+ else
129
+ if @options[:cached]
130
+ raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering"
131
+ end
132
+
133
+ paths.map! { |path| retrieve_variable(path).unshift(path) }
134
+ collection = MixedCollectionIterator.new(collection, paths)
135
+ render_collection(collection, context, nil, nil, nil, block)
136
+ end
137
+ end
138
+
139
+ private
140
+ def retrieve_variable(path)
141
+ variable = local_variable(path)
142
+ [variable, :"#{variable}_counter", :"#{variable}_iteration"]
143
+ end
144
+
145
+ def render_collection(collection, view, path, template, layout, block)
146
+ identifier = (template && template.identifier) || path
147
+ ActiveSupport::Notifications.instrument(
148
+ "render_collection.action_view",
149
+ identifier: identifier,
150
+ layout: layout && layout.virtual_path,
151
+ count: collection.length
152
+ ) do |payload|
153
+ spacer = if @options.key?(:spacer_template)
154
+ spacer_template = find_template(@options[:spacer_template], @locals.keys)
155
+ build_rendered_template(spacer_template.render(view, @locals), spacer_template)
156
+ else
157
+ RenderedTemplate::EMPTY_SPACER
158
+ end
159
+
160
+ collection_body = if template
161
+ cache_collection_render(payload, view, template, collection) do |filtered_collection|
162
+ collection_with_template(view, template, layout, filtered_collection)
163
+ end
164
+ else
165
+ collection_with_template(view, nil, layout, collection)
166
+ end
167
+
168
+ return RenderedCollection.empty(@lookup_context.formats.first) if collection_body.empty?
169
+
170
+ build_rendered_collection(collection_body, spacer)
171
+ end
172
+ end
173
+
174
+ def collection_with_template(view, template, layout, collection)
175
+ locals = @locals
176
+ cache = {}
177
+
178
+ partial_iteration = PartialIteration.new(collection.size)
179
+
180
+ collection.each_with_info.map do |object, (path, as, counter, iteration)|
181
+ index = partial_iteration.index
182
+
183
+ locals[as] = object
184
+ locals[counter] = index
185
+ locals[iteration] = partial_iteration
186
+
187
+ _template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
188
+
189
+ content = _template.render(view, locals)
190
+ content = layout.render(view, locals) { content } if layout
191
+ partial_iteration.iterate!
192
+ build_rendered_template(content, _template)
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ class ObjectRenderer < PartialRenderer # :nodoc:
5
+ include ObjectRendering
6
+
7
+ def initialize(lookup_context, options)
8
+ super
9
+ @object = nil
10
+ @local_name = nil
11
+ end
12
+
13
+ def render_object_with_partial(object, partial, context, block)
14
+ @object = object
15
+ @local_name = local_variable(partial)
16
+ render(partial, context, block)
17
+ end
18
+
19
+ def render_object_derive_partial(object, context, block)
20
+ path = partial_path(object, context)
21
+ render_object_with_partial(object, path, context, block)
22
+ end
23
+
24
+ private
25
+ def template_keys(path)
26
+ super + [@local_name]
27
+ end
28
+
29
+ def render_partial_template(view, locals, template, layout, block)
30
+ locals[@local_name || template.variable] = @object
31
+ super(view, locals, template, layout, block)
32
+ end
33
+ end
34
+ end
@@ -1,36 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "concurrent/map"
4
3
  require "action_view/renderer/partial_renderer/collection_caching"
5
4
 
6
5
  module ActionView
7
- class PartialIteration
8
- # The number of iterations that will be done by the partial.
9
- attr_reader :size
10
-
11
- # The current iteration of the partial.
12
- attr_reader :index
13
-
14
- def initialize(size)
15
- @size = size
16
- @index = 0
17
- end
18
-
19
- # Check if this is the first iteration of the partial.
20
- def first?
21
- index == 0
22
- end
23
-
24
- # Check if this is the last iteration of the partial.
25
- def last?
26
- index == size - 1
27
- end
28
-
29
- def iterate! # :nodoc:
30
- @index += 1
31
- end
32
- end
33
-
34
6
  # = Action View Partials
35
7
  #
36
8
  # There's also a convenience method for rendering sub templates within the current controller that depends on a
@@ -105,9 +77,6 @@ module ActionView
105
77
  #
106
78
  # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
107
79
  #
108
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
109
- # just keep domain objects, like Active Records, in there.
110
- #
111
80
  # == \Rendering shared partials
112
81
  #
113
82
  # Two controllers can share a set of partials and render them like this:
@@ -285,268 +254,47 @@ module ActionView
285
254
  class PartialRenderer < AbstractRenderer
286
255
  include CollectionCaching
287
256
 
288
- PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
289
- h[k] = Concurrent::Map.new
257
+ def initialize(lookup_context, options)
258
+ super(lookup_context)
259
+ @options = options
260
+ @locals = @options[:locals] || {}
261
+ @details = extract_details(@options)
290
262
  end
291
263
 
292
- def initialize(*)
293
- super
294
- @context_prefix = @lookup_context.prefixes.first
295
- end
296
-
297
- def render(context, options, block)
298
- setup(context, options, block)
299
- @template = find_partial
264
+ def render(partial, context, block)
265
+ template = find_template(partial, template_keys(partial))
300
266
 
301
- @lookup_context.rendered_format ||= begin
302
- if @template && @template.formats.present?
303
- @template.formats.first
304
- else
305
- formats.first
306
- end
267
+ if !block && (layout = @options[:layout])
268
+ layout = find_template(layout.to_s, template_keys(partial))
307
269
  end
308
270
 
309
- if @collection
310
- render_collection
311
- else
312
- render_partial
313
- end
271
+ render_partial_template(context, @locals, template, layout, block)
314
272
  end
315
273
 
316
274
  private
317
-
318
- def render_collection
319
- instrument(:collection, count: @collection.size) do |payload|
320
- return nil if @collection.blank?
321
-
322
- if @options.key?(:spacer_template)
323
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
324
- end
325
-
326
- cache_collection_render(payload) do
327
- @template ? collection_with_template : collection_without_template
328
- end.join(spacer).html_safe
329
- end
275
+ def template_keys(_)
276
+ @locals.keys
330
277
  end
331
278
 
332
- def render_partial
333
- instrument(:partial) do |payload|
334
- view, locals, block = @view, @locals, @block
335
- object, as = @object, @variable
336
-
337
- if !block && (layout = @options[:layout])
338
- layout = find_template(layout.to_s, @template_keys)
339
- end
340
-
341
- object = locals[as] if object.nil? # Respect object when object is false
342
- locals[as] = object if @has_object
343
-
344
- content = @template.render(view, locals) do |*name|
279
+ def render_partial_template(view, locals, template, layout, block)
280
+ ActiveSupport::Notifications.instrument(
281
+ "render_partial.action_view",
282
+ identifier: template.identifier,
283
+ layout: layout && layout.virtual_path
284
+ ) do |payload|
285
+ content = template.render(view, locals, add_to_stack: !block) do |*name|
345
286
  view._layout_for(*name, &block)
346
287
  end
347
288
 
348
289
  content = layout.render(view, locals) { content } if layout
349
- payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path]
350
- content
351
- end
352
- end
353
-
354
- # Sets up instance variables needed for rendering a partial. This method
355
- # finds the options and details and extracts them. The method also contains
356
- # logic that handles the type of object passed in as the partial.
357
- #
358
- # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
359
- # set to that string. Otherwise, the +options[:partial]+ object must
360
- # respond to +to_partial_path+ in order to setup the path.
361
- def setup(context, options, block)
362
- @view = context
363
- @options = options
364
- @block = block
365
-
366
- @locals = options[:locals] || {}
367
- @details = extract_details(options)
368
-
369
- prepend_formats(options[:formats])
370
-
371
- partial = options[:partial]
372
-
373
- if String === partial
374
- @has_object = options.key?(:object)
375
- @object = options[:object]
376
- @collection = collection_from_options
377
- @path = partial
378
- else
379
- @has_object = true
380
- @object = partial
381
- @collection = collection_from_object || collection_from_options
382
-
383
- if @collection
384
- paths = @collection_data = @collection.map { |o| partial_path(o) }
385
- @path = paths.uniq.one? ? paths.first : nil
386
- else
387
- @path = partial_path
388
- end
290
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
291
+ build_rendered_template(content, template)
389
292
  end
390
-
391
- if as = options[:as]
392
- raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
393
- as = as.to_sym
394
- end
395
-
396
- if @path
397
- @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
398
- @template_keys = retrieve_template_keys
399
- else
400
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
401
- end
402
-
403
- self
404
- end
405
-
406
- def collection_from_options
407
- if @options.key?(:collection)
408
- collection = @options[:collection]
409
- collection ? collection.to_a : []
410
- end
411
- end
412
-
413
- def collection_from_object
414
- @object.to_ary if @object.respond_to?(:to_ary)
415
- end
416
-
417
- def find_partial
418
- find_template(@path, @template_keys) if @path
419
293
  end
420
294
 
421
295
  def find_template(path, locals)
422
296
  prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
423
297
  @lookup_context.find_template(path, prefixes, true, locals, @details)
424
298
  end
425
-
426
- def collection_with_template
427
- view, locals, template = @view, @locals, @template
428
- as, counter, iteration = @variable, @variable_counter, @variable_iteration
429
-
430
- if layout = @options[:layout]
431
- layout = find_template(layout, @template_keys)
432
- end
433
-
434
- partial_iteration = PartialIteration.new(@collection.size)
435
- locals[iteration] = partial_iteration
436
-
437
- @collection.map do |object|
438
- locals[as] = object
439
- locals[counter] = partial_iteration.index
440
-
441
- content = template.render(view, locals)
442
- content = layout.render(view, locals) { content } if layout
443
- partial_iteration.iterate!
444
- content
445
- end
446
- end
447
-
448
- def collection_without_template
449
- view, locals, collection_data = @view, @locals, @collection_data
450
- cache = {}
451
- keys = @locals.keys
452
-
453
- partial_iteration = PartialIteration.new(@collection.size)
454
-
455
- @collection.map do |object|
456
- index = partial_iteration.index
457
- path, as, counter, iteration = collection_data[index]
458
-
459
- locals[as] = object
460
- locals[counter] = index
461
- locals[iteration] = partial_iteration
462
-
463
- template = (cache[path] ||= find_template(path, keys + [as, counter, iteration]))
464
- content = template.render(view, locals)
465
- partial_iteration.iterate!
466
- content
467
- end
468
- end
469
-
470
- # Obtains the path to where the object's partial is located. If the object
471
- # responds to +to_partial_path+, then +to_partial_path+ will be called and
472
- # will provide the path. If the object does not respond to +to_partial_path+,
473
- # then an +ArgumentError+ is raised.
474
- #
475
- # If +prefix_partial_path_with_controller_namespace+ is true, then this
476
- # method will prefix the partial paths with a namespace.
477
- def partial_path(object = @object)
478
- object = object.to_model if object.respond_to?(:to_model)
479
-
480
- path = if object.respond_to?(:to_partial_path)
481
- object.to_partial_path
482
- else
483
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
484
- end
485
-
486
- if @view.prefix_partial_path_with_controller_namespace
487
- prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
488
- else
489
- path
490
- end
491
- end
492
-
493
- def prefixed_partial_names
494
- @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
495
- end
496
-
497
- def merge_prefix_into_object_path(prefix, object_path)
498
- if prefix.include?(?/) && object_path.include?(?/)
499
- prefixes = []
500
- prefix_array = File.dirname(prefix).split("/")
501
- object_path_array = object_path.split("/")[0..-3] # skip model dir & partial
502
-
503
- prefix_array.each_with_index do |dir, index|
504
- break if dir == object_path_array[index]
505
- prefixes << dir
506
- end
507
-
508
- (prefixes << object_path).join("/")
509
- else
510
- object_path
511
- end
512
- end
513
-
514
- def retrieve_template_keys
515
- keys = @locals.keys
516
- keys << @variable if @has_object || @collection
517
- if @collection
518
- keys << @variable_counter
519
- keys << @variable_iteration
520
- end
521
- keys
522
- end
523
-
524
- def retrieve_variable(path, as)
525
- variable = as || begin
526
- base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
527
- raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
528
- $1.to_sym
529
- end
530
- if @collection
531
- variable_counter = :"#{variable}_counter"
532
- variable_iteration = :"#{variable}_iteration"
533
- end
534
- [variable, variable_counter, variable_iteration]
535
- end
536
-
537
- IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " \
538
- "make sure your partial name starts with underscore."
539
-
540
- OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " \
541
- "make sure it starts with lowercase letter, " \
542
- "and is followed by any combination of letters, numbers and underscores."
543
-
544
- def raise_invalid_identifier(path)
545
- raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
546
- end
547
-
548
- def raise_invalid_option_as(as)
549
- raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
550
- end
551
299
  end
552
300
  end