actionview 4.1.13 → 6.1.3.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 (124) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +181 -359
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +12 -6
  5. data/lib/action_view/base.rb +115 -43
  6. data/lib/action_view/buffers.rb +22 -4
  7. data/lib/action_view/cache_expiry.rb +52 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker.rb +61 -21
  10. data/lib/action_view/digestor.rb +89 -84
  11. data/lib/action_view/flows.rb +12 -13
  12. data/lib/action_view/gem_version.rb +6 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +16 -11
  14. data/lib/action_view/helpers/asset_tag_helper.rb +311 -105
  15. data/lib/action_view/helpers/asset_url_helper.rb +197 -80
  16. data/lib/action_view/helpers/atom_feed_helper.rb +20 -17
  17. data/lib/action_view/helpers/cache_helper.rb +109 -45
  18. data/lib/action_view/helpers/capture_helper.rb +20 -22
  19. data/lib/action_view/helpers/controller_helper.rb +15 -4
  20. data/lib/action_view/helpers/csp_helper.rb +26 -0
  21. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  22. data/lib/action_view/helpers/date_helper.rb +245 -140
  23. data/lib/action_view/helpers/debug_helper.rb +14 -17
  24. data/lib/action_view/helpers/form_helper.rb +875 -148
  25. data/lib/action_view/helpers/form_options_helper.rb +128 -82
  26. data/lib/action_view/helpers/form_tag_helper.rb +253 -91
  27. data/lib/action_view/helpers/javascript_helper.rb +37 -15
  28. data/lib/action_view/helpers/number_helper.rb +100 -77
  29. data/lib/action_view/helpers/output_safety_helper.rb +42 -10
  30. data/lib/action_view/helpers/rendering_helper.rb +26 -15
  31. data/lib/action_view/helpers/sanitize_helper.rb +79 -164
  32. data/lib/action_view/helpers/tag_helper.rb +277 -64
  33. data/lib/action_view/helpers/tags/base.rb +143 -92
  34. data/lib/action_view/helpers/tags/check_box.rb +20 -19
  35. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  36. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -30
  37. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  38. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  39. data/lib/action_view/helpers/tags/collection_select.rb +4 -2
  40. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  41. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  42. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  43. data/lib/action_view/helpers/tags/datetime_field.rb +14 -5
  44. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  45. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  47. data/lib/action_view/helpers/tags/file_field.rb +2 -0
  48. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  49. data/lib/action_view/helpers/tags/hidden_field.rb +2 -0
  50. data/lib/action_view/helpers/tags/label.rb +41 -22
  51. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  52. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  53. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  54. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  55. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  56. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  57. data/lib/action_view/helpers/tags/search_field.rb +3 -0
  58. data/lib/action_view/helpers/tags/select.rb +11 -10
  59. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  60. data/lib/action_view/helpers/tags/text_area.rb +7 -1
  61. data/lib/action_view/helpers/tags/text_field.rb +11 -7
  62. data/lib/action_view/helpers/tags/time_field.rb +3 -2
  63. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  64. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  65. data/lib/action_view/helpers/tags/translator.rb +39 -0
  66. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  67. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  68. data/lib/action_view/helpers/tags.rb +4 -1
  69. data/lib/action_view/helpers/text_helper.rb +80 -45
  70. data/lib/action_view/helpers/translation_helper.rb +148 -67
  71. data/lib/action_view/helpers/url_helper.rb +289 -147
  72. data/lib/action_view/helpers.rb +5 -3
  73. data/lib/action_view/layouts.rb +68 -63
  74. data/lib/action_view/log_subscriber.rb +80 -13
  75. data/lib/action_view/lookup_context.rb +137 -92
  76. data/lib/action_view/model_naming.rb +4 -2
  77. data/lib/action_view/path_set.rb +30 -16
  78. data/lib/action_view/railtie.rb +62 -13
  79. data/lib/action_view/record_identifier.rb +53 -26
  80. data/lib/action_view/renderer/abstract_renderer.rb +152 -13
  81. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  82. data/lib/action_view/renderer/object_renderer.rb +34 -0
  83. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  84. data/lib/action_view/renderer/partial_renderer.rb +61 -261
  85. data/lib/action_view/renderer/renderer.rb +67 -6
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +58 -54
  87. data/lib/action_view/renderer/template_renderer.rb +83 -75
  88. data/lib/action_view/rendering.rb +73 -46
  89. data/lib/action_view/routing_url_for.rb +54 -17
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template/error.rb +44 -29
  92. data/lib/action_view/template/handlers/builder.rb +12 -13
  93. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  94. data/lib/action_view/template/handlers/erb.rb +23 -89
  95. data/lib/action_view/template/handlers/html.rb +11 -0
  96. data/lib/action_view/template/handlers/raw.rb +4 -4
  97. data/lib/action_view/template/handlers.rb +22 -9
  98. data/lib/action_view/template/html.rb +10 -11
  99. data/lib/action_view/template/inline.rb +22 -0
  100. data/lib/action_view/template/raw_file.rb +25 -0
  101. data/lib/action_view/template/renderable.rb +24 -0
  102. data/lib/action_view/template/resolver.rb +267 -181
  103. data/lib/action_view/template/sources/file.rb +17 -0
  104. data/lib/action_view/template/sources.rb +13 -0
  105. data/lib/action_view/template/text.rb +8 -10
  106. data/lib/action_view/template/types.rb +18 -18
  107. data/lib/action_view/template.rb +109 -99
  108. data/lib/action_view/test_case.rb +73 -53
  109. data/lib/action_view/testing/resolvers.rb +24 -33
  110. data/lib/action_view/unbound_template.rb +31 -0
  111. data/lib/action_view/version.rb +3 -1
  112. data/lib/action_view/view_paths.rb +74 -44
  113. data/lib/action_view.rb +14 -9
  114. data/lib/assets/compiled/rails-ujs.js +746 -0
  115. metadata +71 -26
  116. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  117. data/lib/action_view/tasks/dependencies.rake +0 -23
  118. data/lib/action_view/vendor/html-scanner/html/document.rb +0 -68
  119. data/lib/action_view/vendor/html-scanner/html/node.rb +0 -532
  120. data/lib/action_view/vendor/html-scanner/html/sanitizer.rb +0 -188
  121. data/lib/action_view/vendor/html-scanner/html/selector.rb +0 -830
  122. data/lib/action_view/vendor/html-scanner/html/tokenizer.rb +0 -107
  123. data/lib/action_view/vendor/html-scanner/html/version.rb +0 -11
  124. data/lib/action_view/vendor/html-scanner.rb +0 -20
@@ -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
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
4
+
5
+ module ActionView
6
+ module CollectionCaching # :nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ # Fallback cache store if Action View is used without Rails.
11
+ # Otherwise overridden in Railtie to use Rails.cache.
12
+ mattr_accessor :collection_cache, default: ActiveSupport::Cache::MemoryStore.new
13
+ end
14
+
15
+ private
16
+ def will_cache?(options, view)
17
+ options[:cached] && view.controller.respond_to?(:perform_caching) && view.controller.perform_caching
18
+ end
19
+
20
+ def cache_collection_render(instrumentation_payload, view, template, collection)
21
+ return yield(collection) unless will_cache?(@options, view)
22
+
23
+ collection_iterator = collection
24
+
25
+ # Result is a hash with the key represents the
26
+ # key used for cache lookup and the value is the item
27
+ # on which the partial is being rendered
28
+ keyed_collection, ordered_keys = collection_by_cache_keys(view, template, collection)
29
+
30
+ # Pull all partials from cache
31
+ # Result is a hash, key matches the entry in
32
+ # `keyed_collection` where the cache was retrieved and the
33
+ # value is the value that was present in the cache
34
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
35
+ instrumentation_payload[:cache_hits] = cached_partials.size
36
+
37
+ # Extract the items for the keys that are not found
38
+ collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
39
+
40
+ rendered_partials = collection.empty? ? [] : yield(collection_iterator.from_collection(collection))
41
+
42
+ index = 0
43
+ keyed_partials = fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do
44
+ # This block is called once
45
+ # for every cache miss while preserving order.
46
+ rendered_partials[index].tap { index += 1 }
47
+ end
48
+
49
+ ordered_keys.map do |key|
50
+ keyed_partials[key]
51
+ end
52
+ end
53
+
54
+ def callable_cache_key?
55
+ @options[:cached].respond_to?(:call)
56
+ end
57
+
58
+ def collection_by_cache_keys(view, template, collection)
59
+ seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
60
+
61
+ digest_path = view.digest_path_from_template(template)
62
+
63
+ collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
64
+ key = expanded_cache_key(seed.call(item), view, template, digest_path)
65
+ ordered_keys << key
66
+ hash[key] = item
67
+ end
68
+ end
69
+
70
+ def expanded_cache_key(key, view, template, digest_path)
71
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, digest_path: digest_path))
72
+ key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
73
+ end
74
+
75
+ # `order_by` is an enumerable object containing keys of the cache,
76
+ # all keys are passed in whether found already or not.
77
+ #
78
+ # `cached_partials` is a hash. If the value exists
79
+ # it represents the rendered partial from the cache
80
+ # otherwise `Hash#fetch` will take the value of its block.
81
+ #
82
+ # This method expects a block that will return the rendered
83
+ # partial. An example is to render all results
84
+ # for each element that was not found in the cache and store it as an array.
85
+ # Order it so that the first empty cache element in `cached_partials`
86
+ # corresponds to the first element in `rendered_partials`.
87
+ #
88
+ # If the partial is not already cached it will also be
89
+ # written back to the underlying cache store.
90
+ def fetch_or_cache_partial(cached_partials, template, order_by:)
91
+ order_by.index_with do |cache_key|
92
+ if content = cached_partials[cache_key]
93
+ build_rendered_template(content, template)
94
+ else
95
+ yield.tap do |rendered_partial|
96
+ collection_cache.write(cache_key, rendered_partial.body)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -1,4 +1,6 @@
1
- require 'thread_safe'
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/renderer/partial_renderer/collection_caching"
2
4
 
3
5
  module ActionView
4
6
  # = Action View Partials
@@ -22,12 +24,12 @@ module ActionView
22
24
  # <%= render partial: "ad", locals: { ad: ad } %>
23
25
  # <% end %>
24
26
  #
25
- # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then
26
- # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
27
+ # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
28
+ # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
27
29
  #
28
30
  # == The :as and :object options
29
31
  #
30
- # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables.
32
+ # By default ActionView::PartialRenderer doesn't have any local variables.
31
33
  # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
32
34
  #
33
35
  # <%= render partial: "account", object: @buyer %>
@@ -46,7 +48,7 @@ module ActionView
46
48
  #
47
49
  # <%= render partial: "account", locals: { user: @buyer } %>
48
50
  #
49
- # == Rendering a collection of partials
51
+ # == \Rendering a collection of partials
50
52
  #
51
53
  # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
52
54
  # render a sub template for each of the elements. This pattern has been implemented as a single method that
@@ -55,9 +57,13 @@ module ActionView
55
57
  #
56
58
  # <%= render partial: "ad", collection: @advertisements %>
57
59
  #
58
- # This will render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display. An
59
- # iteration counter will automatically be made available to the template with a name of the form
60
- # +partial_name_counter+. In the case of the example above, the template would be fed +ad_counter+.
60
+ # This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
61
+ # iteration object will automatically be made available to the template with a name of the form
62
+ # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
63
+ # the collection and the total size of the collection. The iteration object also has two convenience methods,
64
+ # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
65
+ # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
66
+ # +index+ method.
61
67
  #
62
68
  # The <tt>:as</tt> option may be used when rendering partials.
63
69
  #
@@ -66,37 +72,34 @@ module ActionView
66
72
  #
67
73
  # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
68
74
  #
69
- # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you
70
- # to specify a text which will displayed instead by using this form:
75
+ # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
76
+ # to specify a text which will be displayed instead by using this form:
71
77
  #
72
78
  # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
73
79
  #
74
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
75
- # just keep domain objects, like Active Records, in there.
76
- #
77
- # == Rendering shared partials
80
+ # == \Rendering shared partials
78
81
  #
79
82
  # Two controllers can share a set of partials and render them like this:
80
83
  #
81
84
  # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
82
85
  #
83
- # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
86
+ # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
84
87
  #
85
- # == Rendering objects that respond to `to_partial_path`
88
+ # == \Rendering objects that respond to +to_partial_path+
86
89
  #
87
90
  # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
88
- # and pick the proper path by checking `to_partial_path` method.
91
+ # and pick the proper path by checking +to_partial_path+ method.
89
92
  #
90
93
  # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
91
94
  # # <%= render partial: "accounts/account", locals: { account: @account} %>
92
95
  # <%= render partial: @account %>
93
96
  #
94
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
97
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
95
98
  # # that's why we can replace:
96
99
  # # <%= render partial: "posts/post", collection: @posts %>
97
100
  # <%= render partial: @posts %>
98
101
  #
99
- # == Rendering the default case
102
+ # == \Rendering the default case
100
103
  #
101
104
  # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
102
105
  # defaults of render to render partials. Examples:
@@ -111,34 +114,34 @@ module ActionView
111
114
  # # <%= render partial: "accounts/account", locals: { account: @account} %>
112
115
  # <%= render @account %>
113
116
  #
114
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
117
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
115
118
  # # that's why we can replace:
116
119
  # # <%= render partial: "posts/post", collection: @posts %>
117
120
  # <%= render @posts %>
118
121
  #
119
- # == Rendering partials with layouts
122
+ # == \Rendering partials with layouts
120
123
  #
121
124
  # Partials can have their own layouts applied to them. These layouts are different than the ones that are
122
125
  # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
123
126
  # of users:
124
127
  #
125
- # <%# app/views/users/index.html.erb &>
128
+ # <%# app/views/users/index.html.erb %>
126
129
  # Here's the administrator:
127
130
  # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
128
131
  #
129
132
  # Here's the editor:
130
133
  # <%= render partial: "user", layout: "editor", locals: { user: editor } %>
131
134
  #
132
- # <%# app/views/users/_user.html.erb &>
135
+ # <%# app/views/users/_user.html.erb %>
133
136
  # Name: <%= user.name %>
134
137
  #
135
- # <%# app/views/users/_administrator.html.erb &>
138
+ # <%# app/views/users/_administrator.html.erb %>
136
139
  # <div id="administrator">
137
140
  # Budget: $<%= user.budget %>
138
141
  # <%= yield %>
139
142
  # </div>
140
143
  #
141
- # <%# app/views/users/_editor.html.erb &>
144
+ # <%# app/views/users/_editor.html.erb %>
142
145
  # <div id="editor">
143
146
  # Deadline: <%= user.deadline %>
144
147
  # <%= yield %>
@@ -201,7 +204,7 @@ module ActionView
201
204
  #
202
205
  # You can also apply a layout to a block within any template:
203
206
  #
204
- # <%# app/views/users/_chief.html.erb &>
207
+ # <%# app/views/users/_chief.html.erb %>
205
208
  # <%= render(layout: "administrator", locals: { user: chief }) do %>
206
209
  # Title: <%= chief.title %>
207
210
  # <% end %>
@@ -218,13 +221,13 @@ module ActionView
218
221
  # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
219
222
  # an array to layout and treat it as an enumerable.
220
223
  #
221
- # <%# app/views/users/_user.html.erb &>
224
+ # <%# app/views/users/_user.html.erb %>
222
225
  # <div class="user">
223
226
  # Budget: $<%= user.budget %>
224
227
  # <%= yield user %>
225
228
  # </div>
226
229
  #
227
- # <%# app/views/users/index.html.erb &>
230
+ # <%# app/views/users/index.html.erb %>
228
231
  # <%= render layout: @users do |user| %>
229
232
  # Title: <%= user.title %>
230
233
  # <% end %>
@@ -233,14 +236,14 @@ module ActionView
233
236
  #
234
237
  # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
235
238
  #
236
- # <%# app/views/users/_user.html.erb &>
239
+ # <%# app/views/users/_user.html.erb %>
237
240
  # <div class="user">
238
241
  # <%= yield user, :header %>
239
242
  # Budget: $<%= user.budget %>
240
243
  # <%= yield user, :footer %>
241
244
  # </div>
242
245
  #
243
- # <%# app/views/users/index.html.erb &>
246
+ # <%# app/views/users/index.html.erb %>
244
247
  # <%= render layout: @users do |user, section| %>
245
248
  # <%- case section when :header -%>
246
249
  # Title: <%= user.title %>
@@ -249,252 +252,49 @@ module ActionView
249
252
  # <%- end -%>
250
253
  # <% end %>
251
254
  class PartialRenderer < AbstractRenderer
252
- PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
253
- h[k] = ThreadSafe::Cache.new
254
- end
255
-
256
- def initialize(*)
257
- super
258
- @context_prefix = @lookup_context.prefixes.first
259
- end
260
-
261
- def render(context, options, block)
262
- setup(context, options, block)
263
- identifier = (@template = find_partial) ? @template.identifier : @path
264
-
265
- @lookup_context.rendered_format ||= begin
266
- if @template && @template.formats.present?
267
- @template.formats.first
268
- else
269
- formats.first
270
- end
271
- end
272
-
273
- if @collection
274
- instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
275
- render_collection
276
- end
277
- else
278
- instrument(:partial, :identifier => identifier) do
279
- render_partial
280
- end
281
- end
282
- end
283
-
284
- def render_collection
285
- return nil if @collection.blank?
255
+ include CollectionCaching
286
256
 
287
- if @options.key?(:spacer_template)
288
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
289
- end
290
-
291
- result = @template ? collection_with_template : collection_without_template
292
- result.join(spacer).html_safe
257
+ def initialize(lookup_context, options)
258
+ super(lookup_context)
259
+ @options = options
260
+ @locals = @options[:locals] || {}
261
+ @details = extract_details(@options)
293
262
  end
294
263
 
295
- def render_partial
296
- view, locals, block = @view, @locals, @block
297
- object, as = @object, @variable
264
+ def render(partial, context, block)
265
+ template = find_template(partial, template_keys(partial))
298
266
 
299
267
  if !block && (layout = @options[:layout])
300
- layout = find_template(layout.to_s, @template_keys)
301
- end
302
-
303
- object ||= locals[as]
304
- locals[as] = object
305
-
306
- content = @template.render(view, locals) do |*name|
307
- view._layout_for(*name, &block)
268
+ layout = find_template(layout.to_s, template_keys(partial))
308
269
  end
309
270
 
310
- content = layout.render(view, locals){ content } if layout
311
- content
271
+ render_partial_template(context, @locals, template, layout, block)
312
272
  end
313
273
 
314
274
  private
315
-
316
- # Sets up instance variables needed for rendering a partial. This method
317
- # finds the options and details and extracts them. The method also contains
318
- # logic that handles the type of object passed in as the partial.
319
- #
320
- # If +options[:partial]+ is a string, then the +@path+ instance variable is
321
- # set to that string. Otherwise, the +options[:partial]+ object must
322
- # respond to +to_partial_path+ in order to setup the path.
323
- def setup(context, options, block)
324
- @view = context
325
- partial = options[:partial]
326
-
327
- @options = options
328
- @locals = options[:locals] || {}
329
- @block = block
330
- @details = extract_details(options)
331
-
332
- prepend_formats(options[:formats])
333
-
334
- if String === partial
335
- @object = options[:object] if options.has_key?(:object)
336
- @path = partial
337
- @collection = collection
338
- else
339
- @object = partial
340
-
341
- if @collection = collection_from_object || collection
342
- paths = @collection_data = @collection.map { |o| partial_path(o) }
343
- @path = paths.uniq.size == 1 ? paths.first : nil
344
- else
345
- @path = partial_path
346
- end
347
- end
348
-
349
- if as = options[:as]
350
- raise_invalid_option_as(as) unless as.to_s =~ /\A[a-z_]\w*\z/
351
- as = as.to_sym
352
- end
353
-
354
- if @path
355
- @variable, @variable_counter = retrieve_variable(@path, as)
356
- @template_keys = retrieve_template_keys
357
- else
358
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
275
+ def template_keys(_)
276
+ @locals.keys
359
277
  end
360
278
 
361
- self
362
- end
363
-
364
- def collection
365
- if @options.key?(:collection)
366
- collection = @options[:collection]
367
- collection.respond_to?(:to_ary) ? collection.to_ary : []
368
- end
369
- end
370
-
371
- def collection_from_object
372
- @object.to_ary if @object.respond_to?(:to_ary)
373
- end
374
-
375
- def find_partial
376
- if path = @path
377
- find_template(path, @template_keys)
378
- end
379
- end
380
-
381
- def find_template(path, locals)
382
- prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
383
- @lookup_context.find_template(path, prefixes, true, locals, @details)
384
- end
385
-
386
- def collection_with_template
387
- view, locals, template = @view, @locals, @template
388
- as, counter = @variable, @variable_counter
389
-
390
- if layout = @options[:layout]
391
- layout = find_template(layout, @template_keys)
392
- end
393
-
394
- index = -1
395
- @collection.map do |object|
396
- locals[as] = object
397
- locals[counter] = (index += 1)
398
-
399
- content = template.render(view, locals)
400
- content = layout.render(view, locals) { content } if layout
401
- content
402
- end
403
- end
404
-
405
- def collection_without_template
406
- view, locals, collection_data = @view, @locals, @collection_data
407
- cache = {}
408
- keys = @locals.keys
409
-
410
- index = -1
411
- @collection.map do |object|
412
- index += 1
413
- path, as, counter = collection_data[index]
414
-
415
- locals[as] = object
416
- locals[counter] = index
417
-
418
- template = (cache[path] ||= find_template(path, keys + [as, counter]))
419
- template.render(view, locals)
420
- end
421
- end
422
-
423
- # Obtains the path to where the object's partial is located. If the object
424
- # responds to +to_partial_path+, then +to_partial_path+ will be called and
425
- # will provide the path. If the object does not respond to +to_partial_path+,
426
- # then an +ArgumentError+ is raised.
427
- #
428
- # If +prefix_partial_path_with_controller_namespace+ is true, then this
429
- # method will prefix the partial paths with a namespace.
430
- def partial_path(object = @object)
431
- object = object.to_model if object.respond_to?(:to_model)
432
-
433
- path = if object.respond_to?(:to_partial_path)
434
- object.to_partial_path
435
- else
436
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
437
- end
438
-
439
- if @view.prefix_partial_path_with_controller_namespace
440
- prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
441
- else
442
- path
443
- end
444
- end
445
-
446
- def prefixed_partial_names
447
- @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
448
- end
449
-
450
- def merge_prefix_into_object_path(prefix, object_path)
451
- if prefix.include?(?/) && object_path.include?(?/)
452
- prefixes = []
453
- prefix_array = File.dirname(prefix).split('/')
454
- object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
455
-
456
- prefix_array.each_with_index do |dir, index|
457
- break if dir == object_path_array[index]
458
- prefixes << dir
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|
286
+ view._layout_for(*name, &block)
287
+ end
288
+
289
+ content = layout.render(view, locals) { content } if layout
290
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
291
+ build_rendered_template(content, template)
459
292
  end
460
-
461
- (prefixes << object_path).join("/")
462
- else
463
- object_path
464
293
  end
465
- end
466
-
467
- def retrieve_template_keys
468
- keys = @locals.keys
469
- keys << @variable if defined?(@object) || @collection
470
- keys << @variable_counter if @collection
471
- keys
472
- end
473
294
 
474
- def retrieve_variable(path, as)
475
- variable = as || begin
476
- base = path[-1] == "/" ? "" : File.basename(path)
477
- raise_invalid_identifier(path) unless base =~ /\A_?([a-z]\w*)(\.\w+)*\z/
478
- $1.to_sym
295
+ def find_template(path, locals)
296
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
297
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
479
298
  end
480
- variable_counter = :"#{variable}_counter" if @collection
481
- [variable, variable_counter]
482
- end
483
-
484
- IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
485
- "make sure your partial name starts with underscore, " +
486
- "and is followed by any combination of letters, numbers and underscores."
487
-
488
- OPTION_AS_ERROR_MESSAGE = "The value (%s) of the option `as` is not a valid Ruby identifier; " +
489
- "make sure it starts with lowercase letter, " +
490
- "and is followed by any combination of letters, numbers and underscores."
491
-
492
- def raise_invalid_identifier(path)
493
- raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
494
- end
495
-
496
- def raise_invalid_option_as(as)
497
- raise ArgumentError.new(OPTION_AS_ERROR_MESSAGE % (as))
498
- end
499
299
  end
500
300
  end