actionview 4.2.11.1 → 7.0.2.4

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 +4 -4
  2. data/CHANGELOG.md +229 -215
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -8
  5. data/lib/action_view/base.rb +116 -43
  6. data/lib/action_view/buffers.rb +20 -3
  7. data/lib/action_view/cache_expiry.rb +66 -0
  8. data/lib/action_view/context.rb +8 -12
  9. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  10. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  11. data/lib/action_view/dependency_tracker.rb +21 -122
  12. data/lib/action_view/digestor.rb +92 -85
  13. data/lib/action_view/flows.rb +15 -16
  14. data/lib/action_view/gem_version.rb +6 -4
  15. data/lib/action_view/helpers/active_model_helper.rb +17 -12
  16. data/lib/action_view/helpers/asset_tag_helper.rb +356 -101
  17. data/lib/action_view/helpers/asset_url_helper.rb +180 -74
  18. data/lib/action_view/helpers/atom_feed_helper.rb +21 -19
  19. data/lib/action_view/helpers/cache_helper.rb +156 -43
  20. data/lib/action_view/helpers/capture_helper.rb +21 -14
  21. data/lib/action_view/helpers/controller_helper.rb +16 -5
  22. data/lib/action_view/helpers/csp_helper.rb +26 -0
  23. data/lib/action_view/helpers/csrf_helper.rb +8 -6
  24. data/lib/action_view/helpers/date_helper.rb +288 -132
  25. data/lib/action_view/helpers/debug_helper.rb +9 -6
  26. data/lib/action_view/helpers/form_helper.rb +956 -173
  27. data/lib/action_view/helpers/form_options_helper.rb +178 -97
  28. data/lib/action_view/helpers/form_tag_helper.rb +220 -101
  29. data/lib/action_view/helpers/javascript_helper.rb +33 -19
  30. data/lib/action_view/helpers/number_helper.rb +88 -63
  31. data/lib/action_view/helpers/output_safety_helper.rb +38 -6
  32. data/lib/action_view/helpers/rendering_helper.rb +21 -10
  33. data/lib/action_view/helpers/sanitize_helper.rb +31 -32
  34. data/lib/action_view/helpers/tag_helper.rb +332 -71
  35. data/lib/action_view/helpers/tags/base.rb +123 -99
  36. data/lib/action_view/helpers/tags/check_box.rb +21 -20
  37. data/lib/action_view/helpers/tags/checkable.rb +4 -2
  38. data/lib/action_view/helpers/tags/collection_check_boxes.rb +12 -34
  39. data/lib/action_view/helpers/tags/collection_helpers.rb +69 -36
  40. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +6 -12
  41. data/lib/action_view/helpers/tags/collection_select.rb +5 -3
  42. data/lib/action_view/helpers/tags/color_field.rb +4 -3
  43. data/lib/action_view/helpers/tags/date_field.rb +3 -2
  44. data/lib/action_view/helpers/tags/date_select.rb +38 -37
  45. data/lib/action_view/helpers/tags/datetime_field.rb +4 -3
  46. data/lib/action_view/helpers/tags/datetime_local_field.rb +3 -2
  47. data/lib/action_view/helpers/tags/datetime_select.rb +2 -0
  48. data/lib/action_view/helpers/tags/email_field.rb +2 -0
  49. data/lib/action_view/helpers/tags/file_field.rb +18 -0
  50. data/lib/action_view/helpers/tags/grouped_collection_select.rb +4 -2
  51. data/lib/action_view/helpers/tags/hidden_field.rb +6 -0
  52. data/lib/action_view/helpers/tags/label.rb +7 -2
  53. data/lib/action_view/helpers/tags/month_field.rb +3 -2
  54. data/lib/action_view/helpers/tags/number_field.rb +2 -0
  55. data/lib/action_view/helpers/tags/password_field.rb +3 -1
  56. data/lib/action_view/helpers/tags/placeholderable.rb +3 -1
  57. data/lib/action_view/helpers/tags/radio_button.rb +7 -6
  58. data/lib/action_view/helpers/tags/range_field.rb +2 -0
  59. data/lib/action_view/helpers/tags/search_field.rb +14 -9
  60. data/lib/action_view/helpers/tags/select.rb +11 -10
  61. data/lib/action_view/helpers/tags/tel_field.rb +2 -0
  62. data/lib/action_view/helpers/tags/text_area.rb +4 -2
  63. data/lib/action_view/helpers/tags/text_field.rb +8 -8
  64. data/lib/action_view/helpers/tags/time_field.rb +12 -2
  65. data/lib/action_view/helpers/tags/time_select.rb +2 -0
  66. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -1
  67. data/lib/action_view/helpers/tags/translator.rb +15 -16
  68. data/lib/action_view/helpers/tags/url_field.rb +2 -0
  69. data/lib/action_view/helpers/tags/week_field.rb +3 -2
  70. data/lib/action_view/helpers/tags/weekday_select.rb +28 -0
  71. data/lib/action_view/helpers/tags.rb +5 -2
  72. data/lib/action_view/helpers/text_helper.rb +80 -51
  73. data/lib/action_view/helpers/translation_helper.rb +120 -69
  74. data/lib/action_view/helpers/url_helper.rb +398 -171
  75. data/lib/action_view/helpers.rb +29 -27
  76. data/lib/action_view/layouts.rb +68 -63
  77. data/lib/action_view/log_subscriber.rb +77 -10
  78. data/lib/action_view/lookup_context.rb +137 -113
  79. data/lib/action_view/model_naming.rb +4 -2
  80. data/lib/action_view/path_set.rb +28 -32
  81. data/lib/action_view/railtie.rb +74 -13
  82. data/lib/action_view/record_identifier.rb +53 -26
  83. data/lib/action_view/render_parser.rb +188 -0
  84. data/lib/action_view/renderer/abstract_renderer.rb +152 -15
  85. data/lib/action_view/renderer/collection_renderer.rb +196 -0
  86. data/lib/action_view/renderer/object_renderer.rb +34 -0
  87. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +102 -0
  88. data/lib/action_view/renderer/partial_renderer.rb +51 -333
  89. data/lib/action_view/renderer/renderer.rb +68 -11
  90. data/lib/action_view/renderer/streaming_template_renderer.rb +60 -56
  91. data/lib/action_view/renderer/template_renderer.rb +87 -74
  92. data/lib/action_view/rendering.rb +73 -47
  93. data/lib/action_view/ripper_ast_parser.rb +198 -0
  94. data/lib/action_view/routing_url_for.rb +35 -24
  95. data/lib/action_view/tasks/cache_digests.rake +25 -0
  96. data/lib/action_view/template/error.rb +151 -41
  97. data/lib/action_view/template/handlers/builder.rb +12 -13
  98. data/lib/action_view/template/handlers/erb/erubi.rb +89 -0
  99. data/lib/action_view/template/handlers/erb.rb +29 -89
  100. data/lib/action_view/template/handlers/html.rb +11 -0
  101. data/lib/action_view/template/handlers/raw.rb +4 -4
  102. data/lib/action_view/template/handlers.rb +14 -10
  103. data/lib/action_view/template/html.rb +12 -13
  104. data/lib/action_view/template/inline.rb +22 -0
  105. data/lib/action_view/template/raw_file.rb +25 -0
  106. data/lib/action_view/template/renderable.rb +24 -0
  107. data/lib/action_view/template/resolver.rb +139 -300
  108. data/lib/action_view/template/sources/file.rb +17 -0
  109. data/lib/action_view/template/sources.rb +13 -0
  110. data/lib/action_view/template/text.rb +10 -12
  111. data/lib/action_view/template/types.rb +28 -26
  112. data/lib/action_view/template.rb +123 -91
  113. data/lib/action_view/template_details.rb +66 -0
  114. data/lib/action_view/template_path.rb +64 -0
  115. data/lib/action_view/test_case.rb +70 -53
  116. data/lib/action_view/testing/resolvers.rb +25 -35
  117. data/lib/action_view/unbound_template.rb +57 -0
  118. data/lib/action_view/version.rb +3 -1
  119. data/lib/action_view/view_paths.rb +73 -58
  120. data/lib/action_view.rb +16 -11
  121. data/lib/assets/compiled/rails-ujs.js +746 -0
  122. metadata +52 -32
  123. data/lib/action_view/helpers/record_tag_helper.rb +0 -108
  124. data/lib/action_view/tasks/dependencies.rake +0 -23
@@ -1,38 +1,66 @@
1
- require 'active_support/core_ext/module'
2
- require 'action_view/model_naming'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module"
4
+ require "action_view/model_naming"
3
5
 
4
6
  module ActionView
5
- # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
6
- # pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to
7
- # a higher logical level.
7
+ # RecordIdentifier encapsulates methods used by various ActionView helpers
8
+ # to associate records with DOM elements.
8
9
  #
9
- # # routes
10
- # resources :posts
10
+ # Consider for example the following code that form of post:
11
11
  #
12
- # # view
13
- # <%= div_for(post) do %> <div id="post_45" class="post">
14
- # <%= post.body %> What a wonderful world!
15
- # <% end %> </div>
12
+ # <%= form_for(post) do |f| %>
13
+ # <%= f.text_field :body %>
14
+ # <% end %>
16
15
  #
17
- # # controller
18
- # def update
19
- # post = Post.find(params[:id])
20
- # post.update(params[:post])
16
+ # When +post+ is a new, unsaved ActiveRecord::Base instance, the resulting HTML
17
+ # is:
21
18
  #
22
- # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post)
23
- # end
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"
24
38
  #
25
- # As the example above shows, you can stop caring to a large extent what the actual id of the post is.
26
- # You just know that one is being assigned and that the subsequent calls in redirect_to expect that
27
- # same naming convention and allows you to write less code if you follow it.
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
28
56
  module RecordIdentifier
29
57
  extend self
30
58
  extend ModelNaming
31
59
 
32
60
  include ModelNaming
33
61
 
34
- JOIN = '_'.freeze
35
- NEW = 'new'.freeze
62
+ JOIN = "_"
63
+ NEW = "new"
36
64
 
37
65
  # The DOM class convention is to use the singular form of an object or class.
38
66
  #
@@ -66,8 +94,7 @@ module ActionView
66
94
  end
67
95
  end
68
96
 
69
- protected
70
-
97
+ private
71
98
  # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
72
99
  # This can be overwritten to customize the default generated string representation if desired.
73
100
  # If you need to read back a key from a dom_id in order to query for the underlying database record,
@@ -76,9 +103,9 @@ module ActionView
76
103
  # overwritten version of the method. By default, this implementation passes the key string through a
77
104
  # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
78
105
  # make sure yourself that your dom ids are valid, in case you overwrite this method.
79
- def record_key_for_dom_id(record)
106
+ def record_key_for_dom_id(record) # :doc:
80
107
  key = convert_to_model(record).to_key
81
- key ? key.join('_') : key
108
+ key ? key.join(JOIN) : key
82
109
  end
83
110
  end
84
111
  end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view/ripper_ast_parser"
4
+
5
+ module ActionView
6
+ class RenderParser # :nodoc:
7
+ def initialize(name, code)
8
+ @name = name
9
+ @code = code
10
+ @parser = RipperASTParser
11
+ end
12
+
13
+ def render_calls
14
+ render_nodes = @parser.parse_render_nodes(@code)
15
+
16
+ render_nodes.map do |method, nodes|
17
+ nodes.map { |n| send(:parse_render, n) }
18
+ end.flatten.compact
19
+ end
20
+
21
+ private
22
+ def directory
23
+ File.dirname(@name)
24
+ end
25
+
26
+ def resolve_path_directory(path)
27
+ if path.include?("/")
28
+ path
29
+ else
30
+ "#{directory}/#{path}"
31
+ end
32
+ end
33
+
34
+ # Convert
35
+ # render("foo", ...)
36
+ # into either
37
+ # render(template: "foo", ...)
38
+ # or
39
+ # render(partial: "foo", ...)
40
+ def normalize_args(string, options_hash)
41
+ if options_hash
42
+ { partial: string, locals: options_hash }
43
+ else
44
+ { partial: string }
45
+ end
46
+ end
47
+
48
+ def parse_render(node)
49
+ node = node.argument_nodes
50
+
51
+ if (node.length == 1 || node.length == 2) && !node[0].hash?
52
+ if node.length == 1
53
+ options = normalize_args(node[0], nil)
54
+ elsif node.length == 2
55
+ options = normalize_args(node[0], node[1])
56
+ end
57
+
58
+ return nil unless options
59
+
60
+ parse_render_from_options(options)
61
+ elsif node.length == 1 && node[0].hash?
62
+ options = parse_hash_to_symbols(node[0])
63
+
64
+ return nil unless options
65
+
66
+ parse_render_from_options(options)
67
+ else
68
+ nil
69
+ end
70
+ end
71
+
72
+ def parse_hash(node)
73
+ node.hash? && node.to_hash
74
+ end
75
+
76
+ def parse_hash_to_symbols(node)
77
+ hash = parse_hash(node)
78
+
79
+ return unless hash
80
+
81
+ hash.transform_keys do |key_node|
82
+ key = parse_sym(key_node)
83
+
84
+ return unless key
85
+
86
+ key
87
+ end
88
+ end
89
+
90
+ ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
91
+
92
+ RENDER_TYPE_KEYS =
93
+ [:partial, :template, :layout]
94
+
95
+ def parse_render_from_options(options_hash)
96
+ renders = []
97
+ keys = options_hash.keys
98
+
99
+ if (keys & RENDER_TYPE_KEYS).size < 1
100
+ # Must have at least one of render keys
101
+ return nil
102
+ end
103
+
104
+ if (keys - ALL_KNOWN_KEYS).any?
105
+ # de-opt in case of unknown option
106
+ return nil
107
+ end
108
+
109
+ render_type = (keys & RENDER_TYPE_KEYS)[0]
110
+
111
+ node = options_hash[render_type]
112
+
113
+ if node.string?
114
+ template = resolve_path_directory(node.to_string)
115
+ else
116
+ if node.variable_reference?
117
+ dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
118
+ elsif node.vcall?
119
+ dependency = node.variable_name
120
+ elsif node.call?
121
+ dependency = node.call_method_name
122
+ else
123
+ return
124
+ end
125
+
126
+ object_template = true
127
+ template = "#{dependency.pluralize}/#{dependency.singularize}"
128
+ end
129
+
130
+ return unless template
131
+
132
+ if spacer_template = render_template_with_spacer?(options_hash)
133
+ virtual_path = partial_to_virtual_path(:partial, spacer_template)
134
+ renders << virtual_path
135
+ end
136
+
137
+ if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
138
+ return nil if options_hash.key?(:object) && options_hash.key?(:collection)
139
+ return nil unless options_hash.key?(:partial)
140
+ end
141
+
142
+ virtual_path = partial_to_virtual_path(render_type, template)
143
+ renders << virtual_path
144
+
145
+ # Support for rendering multiple templates (i.e. a partial with a layout)
146
+ if layout_template = render_template_with_layout?(render_type, options_hash)
147
+ virtual_path = partial_to_virtual_path(:layout, layout_template)
148
+
149
+ renders << virtual_path
150
+ end
151
+
152
+ renders
153
+ end
154
+
155
+ def parse_str(node)
156
+ node.string? && node.to_string
157
+ end
158
+
159
+ def parse_sym(node)
160
+ node.symbol? && node.to_symbol
161
+ end
162
+
163
+ private
164
+ def render_template_with_layout?(render_type, options_hash)
165
+ if render_type != :layout && options_hash.key?(:layout)
166
+ parse_str(options_hash[:layout])
167
+ end
168
+ end
169
+
170
+ def render_template_with_spacer?(options_hash)
171
+ if options_hash.key?(:spacer_template)
172
+ parse_str(options_hash[:spacer_template])
173
+ end
174
+ end
175
+
176
+ def partial_to_virtual_path(render_type, partial_path)
177
+ if render_type == :partial || render_type == :layout
178
+ partial_path.gsub(%r{(/|^)([^/]*)\z}, '\1_\2')
179
+ else
180
+ partial_path
181
+ end
182
+ end
183
+
184
+ def layout_to_virtual_path(layout_path)
185
+ "layouts/#{layout_path}"
186
+ end
187
+ end
188
+ end
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
1
5
  module ActionView
2
6
  # This class defines the interface for a renderer. Each class that
3
7
  # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
@@ -12,10 +16,10 @@ module ActionView
12
16
  #
13
17
  # Whenever the +render+ method is called on the base +Renderer+ class, a new
14
18
  # renderer object of the correct type is created, and the +render+ method on
15
- # that new object is called in turn. This abstracts the setup and rendering
19
+ # that new object is called in turn. This abstracts the set up and rendering
16
20
  # into a separate classes for partials and templates.
17
- class AbstractRenderer #:nodoc:
18
- delegate :find_template, :find_file, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
21
+ class AbstractRenderer # :nodoc:
22
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
19
23
 
20
24
  def initialize(lookup_context)
21
25
  @lookup_context = lookup_context
@@ -25,25 +29,158 @@ module ActionView
25
29
  raise NotImplementedError
26
30
  end
27
31
 
28
- protected
29
-
30
- def extract_details(options)
31
- @lookup_context.registered_details.each_with_object({}) do |key, details|
32
- value = options[key]
32
+ module ObjectRendering # :nodoc:
33
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
+ h[k] = Concurrent::Map.new
35
+ end
33
36
 
34
- details[key] = Array(value) if value
37
+ def initialize(lookup_context, options)
38
+ super
39
+ @context_prefix = lookup_context.prefixes.first
35
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
36
108
  end
37
109
 
38
- def instrument(name, options={})
39
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
110
+ class RenderedCollection # :nodoc:
111
+ def self.empty(format)
112
+ EmptyCollection.new format
113
+ end
114
+
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
135
+ end
136
+
137
+ def body; nil; end
138
+ end
40
139
  end
41
140
 
42
- def prepend_formats(formats)
43
- formats = Array(formats)
44
- return if formats.empty? || @lookup_context.html_fallback_for_js
141
+ class RenderedTemplate # :nodoc:
142
+ attr_reader :body, :template
45
143
 
46
- @lookup_context.formats = formats | @lookup_context.formats
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
47
154
  end
155
+
156
+ private
157
+ NO_DETAILS = {}.freeze
158
+
159
+ def extract_details(options) # :doc:
160
+ details = nil
161
+ LookupContext.registered_details.each do |key|
162
+ value = options[key]
163
+
164
+ if value
165
+ (details ||= {})[key] = Array(value)
166
+ end
167
+ end
168
+ details || NO_DETAILS
169
+ end
170
+
171
+ def prepend_formats(formats) # :doc:
172
+ formats = Array(formats)
173
+ return if formats.empty? || @lookup_context.html_fallback_for_js
174
+
175
+ @lookup_context.formats = formats | @lookup_context.formats
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
48
185
  end
49
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