actionview 5.2.3

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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +38 -0
  5. data/lib/action_view.rb +97 -0
  6. data/lib/action_view/base.rb +215 -0
  7. data/lib/action_view/buffers.rb +52 -0
  8. data/lib/action_view/context.rb +36 -0
  9. data/lib/action_view/dependency_tracker.rb +175 -0
  10. data/lib/action_view/digestor.rb +134 -0
  11. data/lib/action_view/flows.rb +76 -0
  12. data/lib/action_view/gem_version.rb +17 -0
  13. data/lib/action_view/helpers.rb +68 -0
  14. data/lib/action_view/helpers/active_model_helper.rb +55 -0
  15. data/lib/action_view/helpers/asset_tag_helper.rb +511 -0
  16. data/lib/action_view/helpers/asset_url_helper.rb +469 -0
  17. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  18. data/lib/action_view/helpers/cache_helper.rb +263 -0
  19. data/lib/action_view/helpers/capture_helper.rb +212 -0
  20. data/lib/action_view/helpers/controller_helper.rb +36 -0
  21. data/lib/action_view/helpers/csp_helper.rb +24 -0
  22. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  23. data/lib/action_view/helpers/date_helper.rb +1156 -0
  24. data/lib/action_view/helpers/debug_helper.rb +36 -0
  25. data/lib/action_view/helpers/form_helper.rb +2337 -0
  26. data/lib/action_view/helpers/form_options_helper.rb +887 -0
  27. data/lib/action_view/helpers/form_tag_helper.rb +917 -0
  28. data/lib/action_view/helpers/javascript_helper.rb +94 -0
  29. data/lib/action_view/helpers/number_helper.rb +451 -0
  30. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  31. data/lib/action_view/helpers/record_tag_helper.rb +23 -0
  32. data/lib/action_view/helpers/rendering_helper.rb +99 -0
  33. data/lib/action_view/helpers/sanitize_helper.rb +177 -0
  34. data/lib/action_view/helpers/tag_helper.rb +313 -0
  35. data/lib/action_view/helpers/tags.rb +44 -0
  36. data/lib/action_view/helpers/tags/base.rb +192 -0
  37. data/lib/action_view/helpers/tags/check_box.rb +66 -0
  38. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  39. data/lib/action_view/helpers/tags/collection_check_boxes.rb +36 -0
  40. data/lib/action_view/helpers/tags/collection_helpers.rb +119 -0
  41. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  42. data/lib/action_view/helpers/tags/collection_select.rb +30 -0
  43. data/lib/action_view/helpers/tags/color_field.rb +27 -0
  44. data/lib/action_view/helpers/tags/date_field.rb +15 -0
  45. data/lib/action_view/helpers/tags/date_select.rb +74 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +32 -0
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +21 -0
  48. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  49. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  50. data/lib/action_view/helpers/tags/file_field.rb +10 -0
  51. data/lib/action_view/helpers/tags/grouped_collection_select.rb +31 -0
  52. data/lib/action_view/helpers/tags/hidden_field.rb +10 -0
  53. data/lib/action_view/helpers/tags/label.rb +81 -0
  54. data/lib/action_view/helpers/tags/month_field.rb +15 -0
  55. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  56. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  58. data/lib/action_view/helpers/tags/radio_button.rb +33 -0
  59. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  60. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  61. data/lib/action_view/helpers/tags/select.rb +43 -0
  62. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  63. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  64. data/lib/action_view/helpers/tags/text_field.rb +34 -0
  65. data/lib/action_view/helpers/tags/time_field.rb +15 -0
  66. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  67. data/lib/action_view/helpers/tags/time_zone_select.rb +22 -0
  68. data/lib/action_view/helpers/tags/translator.rb +44 -0
  69. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  70. data/lib/action_view/helpers/tags/week_field.rb +15 -0
  71. data/lib/action_view/helpers/text_helper.rb +486 -0
  72. data/lib/action_view/helpers/translation_helper.rb +141 -0
  73. data/lib/action_view/helpers/url_helper.rb +676 -0
  74. data/lib/action_view/layouts.rb +433 -0
  75. data/lib/action_view/locale/en.yml +56 -0
  76. data/lib/action_view/log_subscriber.rb +96 -0
  77. data/lib/action_view/lookup_context.rb +274 -0
  78. data/lib/action_view/model_naming.rb +14 -0
  79. data/lib/action_view/path_set.rb +100 -0
  80. data/lib/action_view/railtie.rb +82 -0
  81. data/lib/action_view/record_identifier.rb +112 -0
  82. data/lib/action_view/renderer/abstract_renderer.rb +55 -0
  83. data/lib/action_view/renderer/partial_renderer.rb +552 -0
  84. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +57 -0
  85. data/lib/action_view/renderer/renderer.rb +56 -0
  86. data/lib/action_view/renderer/streaming_template_renderer.rb +105 -0
  87. data/lib/action_view/renderer/template_renderer.rb +102 -0
  88. data/lib/action_view/rendering.rb +151 -0
  89. data/lib/action_view/routing_url_for.rb +145 -0
  90. data/lib/action_view/tasks/cache_digests.rake +25 -0
  91. data/lib/action_view/template.rb +361 -0
  92. data/lib/action_view/template/error.rb +141 -0
  93. data/lib/action_view/template/handlers.rb +66 -0
  94. data/lib/action_view/template/handlers/builder.rb +25 -0
  95. data/lib/action_view/template/handlers/erb.rb +74 -0
  96. data/lib/action_view/template/handlers/erb/erubi.rb +83 -0
  97. data/lib/action_view/template/handlers/html.rb +11 -0
  98. data/lib/action_view/template/handlers/raw.rb +11 -0
  99. data/lib/action_view/template/html.rb +34 -0
  100. data/lib/action_view/template/resolver.rb +391 -0
  101. data/lib/action_view/template/text.rb +33 -0
  102. data/lib/action_view/template/types.rb +57 -0
  103. data/lib/action_view/test_case.rb +300 -0
  104. data/lib/action_view/testing/resolvers.rb +54 -0
  105. data/lib/action_view/version.rb +10 -0
  106. data/lib/action_view/view_paths.rb +105 -0
  107. data/lib/assets/compiled/rails-ujs.js +720 -0
  108. metadata +255 -0
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+ require "rails"
5
+
6
+ module ActionView
7
+ # = Action View Railtie
8
+ class Railtie < Rails::Engine # :nodoc:
9
+ config.action_view = ActiveSupport::OrderedOptions.new
10
+ config.action_view.embed_authenticity_token_in_remote_forms = nil
11
+ config.action_view.debug_missing_translation = true
12
+
13
+ config.eager_load_namespaces << ActionView
14
+
15
+ initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
16
+ ActiveSupport.on_load(:action_view) do
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
+ end
21
+
22
+ initializer "action_view.form_with_generates_remote_forms" do |app|
23
+ ActiveSupport.on_load(:action_view) do
24
+ form_with_generates_remote_forms = app.config.action_view.delete(:form_with_generates_remote_forms)
25
+ ActionView::Helpers::FormHelper.form_with_generates_remote_forms = form_with_generates_remote_forms
26
+ end
27
+ end
28
+
29
+ initializer "action_view.form_with_generates_ids" do |app|
30
+ ActiveSupport.on_load(:action_view) do
31
+ form_with_generates_ids = app.config.action_view.delete(:form_with_generates_ids)
32
+ unless form_with_generates_ids.nil?
33
+ ActionView::Helpers::FormHelper.form_with_generates_ids = form_with_generates_ids
34
+ end
35
+ end
36
+ end
37
+
38
+ initializer "action_view.logger" do
39
+ ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
40
+ end
41
+
42
+ initializer "action_view.set_configs" do |app|
43
+ ActiveSupport.on_load(:action_view) do
44
+ app.config.action_view.each do |k, v|
45
+ send "#{k}=", v
46
+ end
47
+ end
48
+ end
49
+
50
+ initializer "action_view.caching" do |app|
51
+ ActiveSupport.on_load(:action_view) do
52
+ if app.config.action_view.cache_template_loading.nil?
53
+ ActionView::Resolver.caching = app.config.cache_classes
54
+ end
55
+ end
56
+ end
57
+
58
+ initializer "action_view.per_request_digest_cache" do |app|
59
+ ActiveSupport.on_load(:action_view) do
60
+ unless ActionView::Resolver.caching?
61
+ app.executor.to_run ActionView::Digestor::PerExecutionDigestCacheExpiry
62
+ end
63
+ end
64
+ end
65
+
66
+ initializer "action_view.setup_action_pack" do |app|
67
+ ActiveSupport.on_load(:action_controller) do
68
+ ActionView::RoutingUrlFor.include(ActionDispatch::Routing::UrlFor)
69
+ end
70
+ end
71
+
72
+ initializer "action_view.collection_caching", after: "action_controller.set_configs" do |app|
73
+ PartialRenderer.collection_cache = app.config.action_controller.cache_store
74
+ end
75
+
76
+ rake_tasks do |app|
77
+ unless app.config.api_only
78
+ load "action_view/tasks/cache_digests.rake"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module"
4
+ require "action_view/model_naming"
5
+
6
+ module ActionView
7
+ # RecordIdentifier encapsulates methods used by various ActionView helpers
8
+ # to associate records with DOM elements.
9
+ #
10
+ # Consider for example the following code that form of post:
11
+ #
12
+ # <%= form_for(post) do |f| %>
13
+ # <%= f.text_field :body %>
14
+ # <% end %>
15
+ #
16
+ # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
17
+ # is:
18
+ #
19
+ # <form class="new_post" id="new_post" action="/posts" accept-charset="UTF-8" method="post">
20
+ # <input type="text" name="post[body]" id="post_body" />
21
+ # </form>
22
+ #
23
+ # When +post+ is a persisted ActiveRecord::Base instance, the resulting HTML
24
+ # is:
25
+ #
26
+ # <form class="edit_post" id="edit_post_42" action="/posts/42" accept-charset="UTF-8" method="post">
27
+ # <input type="text" value="What a wonderful world!" name="post[body]" id="post_body" />
28
+ # </form>
29
+ #
30
+ # In both cases, the +id+ and +class+ of the wrapping DOM element are
31
+ # automatically generated, following naming conventions encapsulated by the
32
+ # RecordIdentifier methods #dom_id and #dom_class:
33
+ #
34
+ # dom_id(Post.new) # => "new_post"
35
+ # dom_class(Post.new) # => "post"
36
+ # dom_id(Post.find 42) # => "post_42"
37
+ # dom_class(Post.find 42) # => "post"
38
+ #
39
+ # Note that these methods do not strictly require +Post+ to be a subclass of
40
+ # ActiveRecord::Base.
41
+ # Any +Post+ class will work as long as its instances respond to +to_key+
42
+ # and +model_name+, given that +model_name+ responds to +param_key+.
43
+ # For instance:
44
+ #
45
+ # class Post
46
+ # attr_accessor :to_key
47
+ #
48
+ # def model_name
49
+ # OpenStruct.new param_key: 'post'
50
+ # end
51
+ #
52
+ # def self.find(id)
53
+ # new.tap { |post| post.to_key = [id] }
54
+ # end
55
+ # end
56
+ module RecordIdentifier
57
+ extend self
58
+ extend ModelNaming
59
+
60
+ include ModelNaming
61
+
62
+ JOIN = "_".freeze
63
+ NEW = "new".freeze
64
+
65
+ # The DOM class convention is to use the singular form of an object or class.
66
+ #
67
+ # dom_class(post) # => "post"
68
+ # dom_class(Person) # => "person"
69
+ #
70
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
71
+ #
72
+ # dom_class(post, :edit) # => "edit_post"
73
+ # dom_class(Person, :edit) # => "edit_person"
74
+ def dom_class(record_or_class, prefix = nil)
75
+ singular = model_name_from_record_or_class(record_or_class).param_key
76
+ prefix ? "#{prefix}#{JOIN}#{singular}" : singular
77
+ end
78
+
79
+ # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
80
+ # If no id is found, prefix with "new_" instead.
81
+ #
82
+ # dom_id(Post.find(45)) # => "post_45"
83
+ # dom_id(Post.new) # => "new_post"
84
+ #
85
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
86
+ #
87
+ # dom_id(Post.find(45), :edit) # => "edit_post_45"
88
+ # dom_id(Post.new, :custom) # => "custom_post"
89
+ def dom_id(record, prefix = nil)
90
+ if record_id = record_key_for_dom_id(record)
91
+ "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
92
+ else
93
+ dom_class(record, prefix || NEW)
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
100
+ # This can be overwritten to customize the default generated string representation if desired.
101
+ # If you need to read back a key from a dom_id in order to query for the underlying database record,
102
+ # you should write a helper like 'person_record_from_dom_id' that will extract the key either based
103
+ # on the default implementation (which just joins all key attributes with '_') or on your own
104
+ # overwritten version of the method. By default, this implementation passes the key string through a
105
+ # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
106
+ # make sure yourself that your dom ids are valid, in case you overwrite this method.
107
+ def record_key_for_dom_id(record) # :doc:
108
+ key = convert_to_model(record).to_key
109
+ key ? key.join(JOIN) : key
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ # This class defines the interface for a renderer. Each class that
5
+ # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
6
+ # render a specific type of object.
7
+ #
8
+ # The base +Renderer+ class uses its +render+ method to delegate to the
9
+ # renderers. These currently consist of
10
+ #
11
+ # PartialRenderer - Used for rendering partials
12
+ # TemplateRenderer - Used for rendering other types of templates
13
+ # StreamingTemplateRenderer - Used for streaming
14
+ #
15
+ # Whenever the +render+ method is called on the base +Renderer+ class, a new
16
+ # 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
18
+ # into a separate classes for partials and templates.
19
+ class AbstractRenderer #:nodoc:
20
+ delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
21
+
22
+ def initialize(lookup_context)
23
+ @lookup_context = lookup_context
24
+ end
25
+
26
+ def render
27
+ raise NotImplementedError
28
+ end
29
+
30
+ private
31
+
32
+ def extract_details(options) # :doc:
33
+ @lookup_context.registered_details.each_with_object({}) do |key, details|
34
+ value = options[key]
35
+
36
+ details[key] = Array(value) if value
37
+ end
38
+ end
39
+
40
+ def instrument(name, **options) # :doc:
41
+ options[:identifier] ||= (@template && @template.identifier) || @path
42
+
43
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
44
+ yield payload
45
+ end
46
+ end
47
+
48
+ def prepend_formats(formats) # :doc:
49
+ formats = Array(formats)
50
+ return if formats.empty? || @lookup_context.html_fallback_for_js
51
+
52
+ @lookup_context.formats = formats | @lookup_context.formats
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,552 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+ require "action_view/renderer/partial_renderer/collection_caching"
5
+
6
+ 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
+ # = Action View Partials
35
+ #
36
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
37
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
38
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
39
+ # templates that could be rendered on their own.
40
+ #
41
+ # In a template for Advertiser#account:
42
+ #
43
+ # <%= render partial: "account" %>
44
+ #
45
+ # This would render "advertiser/_account.html.erb".
46
+ #
47
+ # In another template for Advertiser#buy, we could have:
48
+ #
49
+ # <%= render partial: "account", locals: { account: @buyer } %>
50
+ #
51
+ # <% @advertisements.each do |ad| %>
52
+ # <%= render partial: "ad", locals: { ad: ad } %>
53
+ # <% end %>
54
+ #
55
+ # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
56
+ # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
57
+ #
58
+ # == The :as and :object options
59
+ #
60
+ # By default ActionView::PartialRenderer doesn't have any local variables.
61
+ # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
62
+ #
63
+ # <%= render partial: "account", object: @buyer %>
64
+ #
65
+ # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
66
+ # equivalent to:
67
+ #
68
+ # <%= render partial: "account", locals: { account: @buyer } %>
69
+ #
70
+ # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
71
+ # wanted it to be +user+ instead of +account+ we'd do:
72
+ #
73
+ # <%= render partial: "account", object: @buyer, as: 'user' %>
74
+ #
75
+ # This is equivalent to
76
+ #
77
+ # <%= render partial: "account", locals: { user: @buyer } %>
78
+ #
79
+ # == \Rendering a collection of partials
80
+ #
81
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
82
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
83
+ # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
84
+ # example in "Using partials" can be rewritten with a single line:
85
+ #
86
+ # <%= render partial: "ad", collection: @advertisements %>
87
+ #
88
+ # This will render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display. An
89
+ # iteration object will automatically be made available to the template with a name of the form
90
+ # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
91
+ # the collection and the total size of the collection. The iteration object also has two convenience methods,
92
+ # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
93
+ # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
94
+ # +index+ method.
95
+ #
96
+ # The <tt>:as</tt> option may be used when rendering partials.
97
+ #
98
+ # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
99
+ # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
100
+ #
101
+ # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
102
+ #
103
+ # If the given <tt>:collection</tt> is +nil+ or empty, <tt>render</tt> will return +nil+. This will allow you
104
+ # to specify a text which will be displayed instead by using this form:
105
+ #
106
+ # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
107
+ #
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
+ # == \Rendering shared partials
112
+ #
113
+ # Two controllers can share a set of partials and render them like this:
114
+ #
115
+ # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
116
+ #
117
+ # This will render the partial <tt>advertisement/_ad.html.erb</tt> regardless of which controller this is being called from.
118
+ #
119
+ # == \Rendering objects that respond to +to_partial_path+
120
+ #
121
+ # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
122
+ # and pick the proper path by checking +to_partial_path+ method.
123
+ #
124
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
125
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
126
+ # <%= render partial: @account %>
127
+ #
128
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
129
+ # # that's why we can replace:
130
+ # # <%= render partial: "posts/post", collection: @posts %>
131
+ # <%= render partial: @posts %>
132
+ #
133
+ # == \Rendering the default case
134
+ #
135
+ # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
136
+ # defaults of render to render partials. Examples:
137
+ #
138
+ # # Instead of <%= render partial: "account" %>
139
+ # <%= render "account" %>
140
+ #
141
+ # # Instead of <%= render partial: "account", locals: { account: @buyer } %>
142
+ # <%= render "account", account: @buyer %>
143
+ #
144
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
145
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
146
+ # <%= render @account %>
147
+ #
148
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on +to_partial_path+,
149
+ # # that's why we can replace:
150
+ # # <%= render partial: "posts/post", collection: @posts %>
151
+ # <%= render @posts %>
152
+ #
153
+ # == \Rendering partials with layouts
154
+ #
155
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
156
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
157
+ # of users:
158
+ #
159
+ # <%# app/views/users/index.html.erb %>
160
+ # Here's the administrator:
161
+ # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
162
+ #
163
+ # Here's the editor:
164
+ # <%= render partial: "user", layout: "editor", locals: { user: editor } %>
165
+ #
166
+ # <%# app/views/users/_user.html.erb %>
167
+ # Name: <%= user.name %>
168
+ #
169
+ # <%# app/views/users/_administrator.html.erb %>
170
+ # <div id="administrator">
171
+ # Budget: $<%= user.budget %>
172
+ # <%= yield %>
173
+ # </div>
174
+ #
175
+ # <%# app/views/users/_editor.html.erb %>
176
+ # <div id="editor">
177
+ # Deadline: <%= user.deadline %>
178
+ # <%= yield %>
179
+ # </div>
180
+ #
181
+ # ...this will return:
182
+ #
183
+ # Here's the administrator:
184
+ # <div id="administrator">
185
+ # Budget: $<%= user.budget %>
186
+ # Name: <%= user.name %>
187
+ # </div>
188
+ #
189
+ # Here's the editor:
190
+ # <div id="editor">
191
+ # Deadline: <%= user.deadline %>
192
+ # Name: <%= user.name %>
193
+ # </div>
194
+ #
195
+ # If a collection is given, the layout will be rendered once for each item in
196
+ # the collection. For example, these two snippets have the same output:
197
+ #
198
+ # <%# app/views/users/_user.html.erb %>
199
+ # Name: <%= user.name %>
200
+ #
201
+ # <%# app/views/users/index.html.erb %>
202
+ # <%# This does not use layouts %>
203
+ # <ul>
204
+ # <% users.each do |user| -%>
205
+ # <li>
206
+ # <%= render partial: "user", locals: { user: user } %>
207
+ # </li>
208
+ # <% end -%>
209
+ # </ul>
210
+ #
211
+ # <%# app/views/users/_li_layout.html.erb %>
212
+ # <li>
213
+ # <%= yield %>
214
+ # </li>
215
+ #
216
+ # <%# app/views/users/index.html.erb %>
217
+ # <ul>
218
+ # <%= render partial: "user", layout: "li_layout", collection: users %>
219
+ # </ul>
220
+ #
221
+ # Given two users whose names are Alice and Bob, these snippets return:
222
+ #
223
+ # <ul>
224
+ # <li>
225
+ # Name: Alice
226
+ # </li>
227
+ # <li>
228
+ # Name: Bob
229
+ # </li>
230
+ # </ul>
231
+ #
232
+ # The current object being rendered, as well as the object_counter, will be
233
+ # available as local variables inside the layout template under the same names
234
+ # as available in the partial.
235
+ #
236
+ # You can also apply a layout to a block within any template:
237
+ #
238
+ # <%# app/views/users/_chief.html.erb %>
239
+ # <%= render(layout: "administrator", locals: { user: chief }) do %>
240
+ # Title: <%= chief.title %>
241
+ # <% end %>
242
+ #
243
+ # ...this will return:
244
+ #
245
+ # <div id="administrator">
246
+ # Budget: $<%= user.budget %>
247
+ # Title: <%= chief.name %>
248
+ # </div>
249
+ #
250
+ # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
251
+ #
252
+ # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
253
+ # an array to layout and treat it as an enumerable.
254
+ #
255
+ # <%# app/views/users/_user.html.erb %>
256
+ # <div class="user">
257
+ # Budget: $<%= user.budget %>
258
+ # <%= yield user %>
259
+ # </div>
260
+ #
261
+ # <%# app/views/users/index.html.erb %>
262
+ # <%= render layout: @users do |user| %>
263
+ # Title: <%= user.title %>
264
+ # <% end %>
265
+ #
266
+ # This will render the layout for each user and yield to the block, passing the user, each time.
267
+ #
268
+ # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
269
+ #
270
+ # <%# app/views/users/_user.html.erb %>
271
+ # <div class="user">
272
+ # <%= yield user, :header %>
273
+ # Budget: $<%= user.budget %>
274
+ # <%= yield user, :footer %>
275
+ # </div>
276
+ #
277
+ # <%# app/views/users/index.html.erb %>
278
+ # <%= render layout: @users do |user, section| %>
279
+ # <%- case section when :header -%>
280
+ # Title: <%= user.title %>
281
+ # <%- when :footer -%>
282
+ # Deadline: <%= user.deadline %>
283
+ # <%- end -%>
284
+ # <% end %>
285
+ class PartialRenderer < AbstractRenderer
286
+ include CollectionCaching
287
+
288
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
289
+ h[k] = Concurrent::Map.new
290
+ end
291
+
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
300
+
301
+ @lookup_context.rendered_format ||= begin
302
+ if @template && @template.formats.present?
303
+ @template.formats.first
304
+ else
305
+ formats.first
306
+ end
307
+ end
308
+
309
+ if @collection
310
+ render_collection
311
+ else
312
+ render_partial
313
+ end
314
+ end
315
+
316
+ 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
330
+ end
331
+
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|
345
+ view._layout_for(*name, &block)
346
+ end
347
+
348
+ 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
389
+ 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
+ end
420
+
421
+ def find_template(path, locals)
422
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
423
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
424
+ 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
+ end
552
+ end