actionview 6.1.7.2 → 7.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +299 -277
  3. data/MIT-LICENSE +2 -1
  4. data/README.rdoc +3 -3
  5. data/app/assets/javascripts/rails-ujs.esm.js +686 -0
  6. data/app/assets/javascripts/rails-ujs.js +630 -0
  7. data/lib/action_view/base.rb +37 -19
  8. data/lib/action_view/buffers.rb +107 -9
  9. data/lib/action_view/cache_expiry.rb +48 -37
  10. data/lib/action_view/context.rb +1 -1
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  12. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  13. data/lib/action_view/dependency_tracker.rb +6 -147
  14. data/lib/action_view/deprecator.rb +7 -0
  15. data/lib/action_view/digestor.rb +8 -5
  16. data/lib/action_view/flows.rb +4 -4
  17. data/lib/action_view/gem_version.rb +4 -4
  18. data/lib/action_view/helpers/active_model_helper.rb +3 -3
  19. data/lib/action_view/helpers/asset_tag_helper.rb +200 -60
  20. data/lib/action_view/helpers/asset_url_helper.rb +22 -21
  21. data/lib/action_view/helpers/atom_feed_helper.rb +8 -9
  22. data/lib/action_view/helpers/cache_helper.rb +55 -12
  23. data/lib/action_view/helpers/capture_helper.rb +34 -14
  24. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  25. data/lib/action_view/helpers/controller_helper.rb +8 -2
  26. data/lib/action_view/helpers/csp_helper.rb +3 -3
  27. data/lib/action_view/helpers/csrf_helper.rb +4 -4
  28. data/lib/action_view/helpers/date_helper.rb +123 -57
  29. data/lib/action_view/helpers/debug_helper.rb +6 -4
  30. data/lib/action_view/helpers/form_helper.rb +253 -97
  31. data/lib/action_view/helpers/form_options_helper.rb +72 -34
  32. data/lib/action_view/helpers/form_tag_helper.rb +189 -58
  33. data/lib/action_view/helpers/javascript_helper.rb +4 -5
  34. data/lib/action_view/helpers/number_helper.rb +43 -335
  35. data/lib/action_view/helpers/output_safety_helper.rb +6 -6
  36. data/lib/action_view/helpers/rendering_helper.rb +6 -7
  37. data/lib/action_view/helpers/sanitize_helper.rb +54 -24
  38. data/lib/action_view/helpers/tag_helper.rb +42 -35
  39. data/lib/action_view/helpers/tags/base.rb +16 -77
  40. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  41. data/lib/action_view/helpers/tags/collection_check_boxes.rb +1 -0
  42. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +1 -0
  43. data/lib/action_view/helpers/tags/collection_select.rb +4 -1
  44. data/lib/action_view/helpers/tags/date_field.rb +1 -1
  45. data/lib/action_view/helpers/tags/date_select.rb +2 -0
  46. data/lib/action_view/helpers/tags/datetime_field.rb +14 -6
  47. data/lib/action_view/helpers/tags/datetime_local_field.rb +11 -2
  48. data/lib/action_view/helpers/tags/file_field.rb +16 -0
  49. data/lib/action_view/helpers/tags/grouped_collection_select.rb +3 -0
  50. data/lib/action_view/helpers/tags/month_field.rb +1 -1
  51. data/lib/action_view/helpers/tags/select.rb +4 -1
  52. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  53. data/lib/action_view/helpers/tags/time_field.rb +11 -2
  54. data/lib/action_view/helpers/tags/time_zone_select.rb +3 -0
  55. data/lib/action_view/helpers/tags/week_field.rb +1 -1
  56. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  57. data/lib/action_view/helpers/tags.rb +5 -2
  58. data/lib/action_view/helpers/text_helper.rb +180 -97
  59. data/lib/action_view/helpers/translation_helper.rb +14 -45
  60. data/lib/action_view/helpers/url_helper.rb +230 -132
  61. data/lib/action_view/helpers.rb +27 -25
  62. data/lib/action_view/layouts.rb +15 -10
  63. data/lib/action_view/log_subscriber.rb +49 -32
  64. data/lib/action_view/lookup_context.rb +58 -61
  65. data/lib/action_view/model_naming.rb +2 -2
  66. data/lib/action_view/path_registry.rb +57 -0
  67. data/lib/action_view/path_set.rb +28 -35
  68. data/lib/action_view/railtie.rb +44 -9
  69. data/lib/action_view/record_identifier.rb +16 -9
  70. data/lib/action_view/render_parser.rb +188 -0
  71. data/lib/action_view/renderer/abstract_renderer.rb +3 -3
  72. data/lib/action_view/renderer/collection_renderer.rb +10 -2
  73. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +21 -3
  74. data/lib/action_view/renderer/partial_renderer.rb +3 -36
  75. data/lib/action_view/renderer/renderer.rb +6 -4
  76. data/lib/action_view/renderer/streaming_template_renderer.rb +6 -5
  77. data/lib/action_view/renderer/template_renderer.rb +9 -4
  78. data/lib/action_view/rendering.rb +25 -7
  79. data/lib/action_view/ripper_ast_parser.rb +198 -0
  80. data/lib/action_view/routing_url_for.rb +8 -5
  81. data/lib/action_view/template/error.rb +122 -14
  82. data/lib/action_view/template/handlers/builder.rb +4 -4
  83. data/lib/action_view/template/handlers/erb/erubi.rb +23 -27
  84. data/lib/action_view/template/handlers/erb.rb +79 -1
  85. data/lib/action_view/template/handlers.rb +4 -4
  86. data/lib/action_view/template/html.rb +4 -4
  87. data/lib/action_view/template/inline.rb +3 -3
  88. data/lib/action_view/template/raw_file.rb +4 -4
  89. data/lib/action_view/template/renderable.rb +1 -1
  90. data/lib/action_view/template/resolver.rb +96 -313
  91. data/lib/action_view/template/text.rb +4 -4
  92. data/lib/action_view/template/types.rb +25 -32
  93. data/lib/action_view/template.rb +245 -41
  94. data/lib/action_view/template_details.rb +66 -0
  95. data/lib/action_view/template_path.rb +66 -0
  96. data/lib/action_view/test_case.rb +182 -23
  97. data/lib/action_view/testing/resolvers.rb +11 -12
  98. data/lib/action_view/unbound_template.rb +43 -7
  99. data/lib/action_view/version.rb +1 -1
  100. data/lib/action_view/view_paths.rb +19 -28
  101. data/lib/action_view.rb +6 -4
  102. data/lib/assets/compiled/rails-ujs.js +36 -5
  103. metadata +32 -25
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
3
+ module ActionView # :nodoc:
4
4
  # = Action View PathSet
5
5
  #
6
6
  # This class is used to store and access paths in Action View. A number of
@@ -8,19 +8,19 @@ module ActionView #:nodoc:
8
8
  # set and also perform operations on other +PathSet+ objects.
9
9
  #
10
10
  # A +LookupContext+ will use a +PathSet+ to store the paths in its context.
11
- class PathSet #:nodoc:
11
+ class PathSet # :nodoc:
12
12
  include Enumerable
13
13
 
14
14
  attr_reader :paths
15
15
 
16
- delegate :[], :include?, :pop, :size, :each, to: :paths
16
+ delegate :[], :include?, :size, :each, to: :paths
17
17
 
18
18
  def initialize(paths = [])
19
- @paths = typecast paths
19
+ @paths = typecast(paths).freeze
20
20
  end
21
21
 
22
22
  def initialize_copy(other)
23
- @paths = other.paths.dup
23
+ @paths = other.paths.dup.freeze
24
24
  self
25
25
  end
26
26
 
@@ -32,58 +32,51 @@ module ActionView #:nodoc:
32
32
  PathSet.new paths.compact
33
33
  end
34
34
 
35
- def +(array)
35
+ def +(other)
36
+ array = Array === other ? other : other.paths
36
37
  PathSet.new(paths + array)
37
38
  end
38
39
 
39
- %w(<< concat push insert unshift).each do |method|
40
- class_eval <<-METHOD, __FILE__, __LINE__ + 1
41
- def #{method}(*args)
42
- paths.#{method}(*typecast(args))
43
- end
44
- METHOD
45
- end
46
-
47
- def find(*args)
48
- find_all(*args).first || raise(MissingTemplate.new(self, *args))
49
- end
50
-
51
- def find_all(path, prefixes = [], *args)
52
- _find_all path, prefixes, args
53
- end
54
-
55
- def exists?(path, prefixes, *args)
56
- find_all(path, prefixes, *args).any?
40
+ def find(path, prefixes, partial, details, details_key, locals)
41
+ find_all(path, prefixes, partial, details, details_key, locals).first ||
42
+ raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
57
43
  end
58
44
 
59
- def find_all_with_query(query) # :nodoc:
60
- paths.each do |resolver|
61
- templates = resolver.find_all_with_query(query)
45
+ def find_all(path, prefixes, partial, details, details_key, locals)
46
+ search_combinations(prefixes) do |resolver, prefix|
47
+ templates = resolver.find_all(path, prefix, partial, details, details_key, locals)
62
48
  return templates unless templates.empty?
63
49
  end
64
-
65
50
  []
66
51
  end
67
52
 
53
+ def exists?(path, prefixes, partial, details, details_key, locals)
54
+ find_all(path, prefixes, partial, details, details_key, locals).any?
55
+ end
56
+
68
57
  private
69
- def _find_all(path, prefixes, args)
70
- prefixes = [prefixes] if String === prefixes
58
+ def search_combinations(prefixes)
59
+ prefixes = Array(prefixes)
71
60
  prefixes.each do |prefix|
72
61
  paths.each do |resolver|
73
- templates = resolver.find_all(path, prefix, *args)
74
- return templates unless templates.empty?
62
+ yield resolver, prefix
75
63
  end
76
64
  end
77
- []
78
65
  end
79
66
 
80
67
  def typecast(paths)
81
68
  paths.map do |path|
82
69
  case path
83
70
  when Pathname, String
84
- OptimizedFileSystemResolver.new path.to_s
85
- else
71
+ # This path should only be reached by "direct" users of
72
+ # ActionView::Base (not using the ViewPaths or Renderer modules).
73
+ # We can't cache/de-dup the file system resolver in this case as we
74
+ # don't know which compiled_method_container we'll be rendering to.
75
+ FileSystemResolver.new(path)
76
+ when Resolver
86
77
  path
78
+ else
79
+ raise TypeError, "#{path.inspect} is not a valid path: must be a String, Pathname, or Resolver"
87
80
  end
88
81
  end
89
82
  end
@@ -10,6 +10,10 @@ module ActionView
10
10
  config.action_view.embed_authenticity_token_in_remote_forms = nil
11
11
  config.action_view.debug_missing_translation = true
12
12
  config.action_view.default_enforce_utf8 = nil
13
+ config.action_view.image_loading = nil
14
+ config.action_view.image_decoding = nil
15
+ config.action_view.apply_stylesheet_media_default = true
16
+ config.action_view.prepend_content_exfiltration_prevention = false
13
17
 
14
18
  config.eager_load_namespaces << ActionView
15
19
 
@@ -38,23 +42,47 @@ module ActionView
38
42
  end
39
43
 
40
44
  config.after_initialize do |app|
45
+ prepend_content_exfiltration_prevention = app.config.action_view.delete(:prepend_content_exfiltration_prevention)
46
+ ActionView::Helpers::ContentExfiltrationPreventionHelper.prepend_content_exfiltration_prevention = prepend_content_exfiltration_prevention
47
+ end
48
+
49
+ config.after_initialize do |app|
50
+ if klass = app.config.action_view.delete(:sanitizer_vendor)
51
+ ActionView::Helpers::SanitizeHelper.sanitizer_vendor = klass
52
+ end
53
+ end
54
+
55
+ config.after_initialize do |app|
56
+ button_to_generates_button_tag = app.config.action_view.delete(:button_to_generates_button_tag)
57
+ unless button_to_generates_button_tag.nil?
58
+ ActionView::Helpers::UrlHelper.button_to_generates_button_tag = button_to_generates_button_tag
59
+ end
60
+ end
61
+
62
+ config.after_initialize do |app|
63
+ frozen_string_literal = app.config.action_view.delete(:frozen_string_literal)
64
+ ActionView::Template.frozen_string_literal = frozen_string_literal
65
+ end
66
+
67
+ config.after_initialize do |app|
68
+ ActionView::Helpers::AssetTagHelper.image_loading = app.config.action_view.delete(:image_loading)
69
+ ActionView::Helpers::AssetTagHelper.image_decoding = app.config.action_view.delete(:image_decoding)
41
70
  ActionView::Helpers::AssetTagHelper.preload_links_header = app.config.action_view.delete(:preload_links_header)
71
+ ActionView::Helpers::AssetTagHelper.apply_stylesheet_media_default = app.config.action_view.delete(:apply_stylesheet_media_default)
42
72
  end
43
73
 
44
74
  config.after_initialize do |app|
45
75
  ActiveSupport.on_load(:action_view) do
46
76
  app.config.action_view.each do |k, v|
47
- if k == :raise_on_missing_translations
48
- ActiveSupport::Deprecation.warn \
49
- "action_view.raise_on_missing_translations is deprecated and will be removed in Rails 7.0. " \
50
- "Set i18n.raise_on_missing_translations instead. " \
51
- "Note that this new setting also affects how missing translations are handled in controllers."
52
- end
53
77
  send "#{k}=", v
54
78
  end
55
79
  end
56
80
  end
57
81
 
82
+ initializer "action_view.deprecator", before: :load_environment_config do |app|
83
+ app.deprecators[:action_view] = ActionView.deprecator
84
+ end
85
+
58
86
  initializer "action_view.logger" do
59
87
  ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
60
88
  end
@@ -62,7 +90,7 @@ module ActionView
62
90
  initializer "action_view.caching" do |app|
63
91
  ActiveSupport.on_load(:action_view) do
64
92
  if app.config.action_view.cache_template_loading.nil?
65
- ActionView::Resolver.caching = app.config.cache_classes
93
+ ActionView::Resolver.caching = !app.config.reloading_enabled?
66
94
  end
67
95
  end
68
96
  end
@@ -79,13 +107,20 @@ module ActionView
79
107
 
80
108
  config.after_initialize do |app|
81
109
  enable_caching = if app.config.action_view.cache_template_loading.nil?
82
- app.config.cache_classes
110
+ !app.config.reloading_enabled?
83
111
  else
84
112
  app.config.action_view.cache_template_loading
85
113
  end
86
114
 
87
115
  unless enable_caching
88
- app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
116
+ view_reloader = ActionView::CacheExpiry::ViewReloader.new(watcher: app.config.file_watcher)
117
+
118
+ app.reloaders << view_reloader
119
+ view_reloader.execute
120
+ app.reloader.to_run do
121
+ require_unload_lock!
122
+ view_reloader.execute
123
+ end
89
124
  end
90
125
  end
91
126
 
@@ -4,6 +4,8 @@ require "active_support/core_ext/module"
4
4
  require "action_view/model_naming"
5
5
 
6
6
  module ActionView
7
+ # = Action View \Record \Identifier
8
+ #
7
9
  # RecordIdentifier encapsulates methods used by various ActionView helpers
8
10
  # to associate records with DOM elements.
9
11
  #
@@ -31,6 +33,8 @@ module ActionView
31
33
  # automatically generated, following naming conventions encapsulated by the
32
34
  # RecordIdentifier methods #dom_id and #dom_class:
33
35
  #
36
+ # dom_id(Post) # => "new_post"
37
+ # dom_class(Post) # => "post"
34
38
  # dom_id(Post.new) # => "new_post"
35
39
  # dom_class(Post.new) # => "post"
36
40
  # dom_id(Post.find 42) # => "post_42"
@@ -79,18 +83,21 @@ module ActionView
79
83
  # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
80
84
  # If no id is found, prefix with "new_" instead.
81
85
  #
82
- # dom_id(Post.find(45)) # => "post_45"
83
- # dom_id(Post.new) # => "new_post"
86
+ # dom_id(Post.find(45)) # => "post_45"
87
+ # dom_id(Post) # => "new_post"
84
88
  #
85
89
  # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
86
90
  #
87
91
  # 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
+ # dom_id(Post, :custom) # => "custom_post"
93
+ def dom_id(record_or_class, prefix = nil)
94
+ raise ArgumentError, "dom_id must be passed a record_or_class as the first argument, you passed #{record_or_class.inspect}" unless record_or_class
95
+
96
+ record_id = record_key_for_dom_id(record_or_class) unless record_or_class.is_a?(Class)
97
+ if record_id
98
+ "#{dom_class(record_or_class, prefix)}#{JOIN}#{record_id}"
92
99
  else
93
- dom_class(record, prefix || NEW)
100
+ dom_class(record_or_class, prefix || NEW)
94
101
  end
95
102
  end
96
103
 
@@ -102,10 +109,10 @@ module ActionView
102
109
  # on the default implementation (which just joins all key attributes with '_') or on your own
103
110
  # overwritten version of the method. By default, this implementation passes the key string through a
104
111
  # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
105
- # make sure yourself that your dom ids are valid, in case you overwrite this method.
112
+ # make sure yourself that your dom ids are valid, in case you override this method.
106
113
  def record_key_for_dom_id(record) # :doc:
107
114
  key = convert_to_model(record).to_key
108
- key ? key.join(JOIN) : key
115
+ key && key.all? ? key.join(JOIN) : nil
109
116
  end
110
117
  end
111
118
  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
@@ -18,7 +18,7 @@ module ActionView
18
18
  # renderer object of the correct type is created, and the +render+ method on
19
19
  # that new object is called in turn. This abstracts the set up and rendering
20
20
  # into a separate classes for partials and templates.
21
- class AbstractRenderer #:nodoc:
21
+ class AbstractRenderer # :nodoc:
22
22
  delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
23
23
 
24
24
  def initialize(lookup_context)
@@ -31,7 +31,7 @@ module ActionView
31
31
 
32
32
  module ObjectRendering # :nodoc:
33
33
  PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
- h[k] = Concurrent::Map.new
34
+ h.compute_if_absent(k) { Concurrent::Map.new }
35
35
  end
36
36
 
37
37
  def initialize(lookup_context, options)
@@ -158,7 +158,7 @@ module ActionView
158
158
 
159
159
  def extract_details(options) # :doc:
160
160
  details = nil
161
- @lookup_context.registered_details.each do |key|
161
+ LookupContext.registered_details.each do |key|
162
162
  value = options[key]
163
163
 
164
164
  if value
@@ -51,6 +51,10 @@ module ActionView
51
51
  def length
52
52
  @collection.respond_to?(:length) ? @collection.length : size
53
53
  end
54
+
55
+ def preload!
56
+ # no-op
57
+ end
54
58
  end
55
59
 
56
60
  class SameCollectionIterator < CollectionIterator # :nodoc:
@@ -84,9 +88,13 @@ module ActionView
84
88
 
85
89
  def each_with_info
86
90
  return super unless block_given?
87
- @relation.preload_associations(@collection)
91
+ preload!
88
92
  super
89
93
  end
94
+
95
+ def preload!
96
+ @relation.preload_associations(@collection)
97
+ end
90
98
  end
91
99
 
92
100
  class MixedCollectionIterator < CollectionIterator # :nodoc:
@@ -186,7 +194,7 @@ module ActionView
186
194
 
187
195
  _template = (cache[path] ||= (template || find_template(path, @locals.keys + [as, counter, iteration])))
188
196
 
189
- content = _template.render(view, locals)
197
+ content = _template.render(view, locals, implicit_locals: [counter, iteration])
190
198
  content = layout.render(view, locals) { content } if layout
191
199
  partial_iteration.iterate!
192
200
  build_rendered_template(content, _template)
@@ -59,6 +59,7 @@ module ActionView
59
59
  seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
60
60
 
61
61
  digest_path = view.digest_path_from_template(template)
62
+ collection.preload! if callable_cache_key?
62
63
 
63
64
  collection.each_with_object([{}, []]) do |item, (hash, ordered_keys)|
64
65
  key = expanded_cache_key(seed.call(item), view, template, digest_path)
@@ -88,15 +89,32 @@ module ActionView
88
89
  # If the partial is not already cached it will also be
89
90
  # written back to the underlying cache store.
90
91
  def fetch_or_cache_partial(cached_partials, template, order_by:)
91
- order_by.index_with do |cache_key|
92
+ entries_to_write = {}
93
+
94
+ keyed_partials = order_by.index_with do |cache_key|
92
95
  if content = cached_partials[cache_key]
93
96
  build_rendered_template(content, template)
94
97
  else
95
- yield.tap do |rendered_partial|
96
- collection_cache.write(cache_key, rendered_partial.body)
98
+ rendered_partial = yield
99
+ body = rendered_partial.body
100
+
101
+ # We want to cache buffers as raw strings. This both improve performance and
102
+ # avoid creating forward compatibility issues with the internal representation
103
+ # of these two types.
104
+ if body.is_a?(ActionView::OutputBuffer) || body.is_a?(ActiveSupport::SafeBuffer)
105
+ body = body.to_str
97
106
  end
107
+
108
+ entries_to_write[cache_key] = body
109
+ rendered_partial
98
110
  end
99
111
  end
112
+
113
+ unless entries_to_write.empty?
114
+ collection_cache.write_multi(entries_to_write)
115
+ end
116
+
117
+ keyed_partials
100
118
  end
101
119
  end
102
120
  end
@@ -27,7 +27,7 @@ module ActionView
27
27
  # This would first render <tt>advertiser/_account.html.erb</tt> with <tt>@buyer</tt> passed in as the local variable +account+, then
28
28
  # render <tt>advertiser/_ad.html.erb</tt> and pass the local variable +ad+ to the template for display.
29
29
  #
30
- # == The :as and :object options
30
+ # == The +:as+ and +:object+ options
31
31
  #
32
32
  # By default ActionView::PartialRenderer doesn't have any local variables.
33
33
  # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
@@ -217,40 +217,6 @@ module ActionView
217
217
  # </div>
218
218
  #
219
219
  # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
220
- #
221
- # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
222
- # an array to layout and treat it as an enumerable.
223
- #
224
- # <%# app/views/users/_user.html.erb %>
225
- # <div class="user">
226
- # Budget: $<%= user.budget %>
227
- # <%= yield user %>
228
- # </div>
229
- #
230
- # <%# app/views/users/index.html.erb %>
231
- # <%= render layout: @users do |user| %>
232
- # Title: <%= user.title %>
233
- # <% end %>
234
- #
235
- # This will render the layout for each user and yield to the block, passing the user, each time.
236
- #
237
- # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
238
- #
239
- # <%# app/views/users/_user.html.erb %>
240
- # <div class="user">
241
- # <%= yield user, :header %>
242
- # Budget: $<%= user.budget %>
243
- # <%= yield user, :footer %>
244
- # </div>
245
- #
246
- # <%# app/views/users/index.html.erb %>
247
- # <%= render layout: @users do |user, section| %>
248
- # <%- case section when :header -%>
249
- # Title: <%= user.title %>
250
- # <%- when :footer -%>
251
- # Deadline: <%= user.deadline %>
252
- # <%- end -%>
253
- # <% end %>
254
220
  class PartialRenderer < AbstractRenderer
255
221
  include CollectionCaching
256
222
 
@@ -280,7 +246,8 @@ module ActionView
280
246
  ActiveSupport::Notifications.instrument(
281
247
  "render_partial.action_view",
282
248
  identifier: template.identifier,
283
- layout: layout && layout.virtual_path
249
+ layout: layout && layout.virtual_path,
250
+ locals: locals
284
251
  ) do |payload|
285
252
  content = template.render(view, locals, add_to_stack: !block) do |*name|
286
253
  view._layout_for(*name, &block)
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
+ # = Action View \Renderer
5
+ #
4
6
  # This is the main entry point for rendering. It basically delegates
5
7
  # to other objects like TemplateRenderer and PartialRenderer which
6
8
  # actually renders the template.
@@ -44,12 +46,12 @@ module ActionView
44
46
  end
45
47
 
46
48
  # Direct access to template rendering.
47
- def render_template(context, options) #:nodoc:
49
+ def render_template(context, options) # :nodoc:
48
50
  render_template_to_object(context, options).body
49
51
  end
50
52
 
51
53
  # Direct access to partial rendering.
52
- def render_partial(context, options, &block) #:nodoc:
54
+ def render_partial(context, options, &block) # :nodoc:
53
55
  render_partial_to_object(context, options, &block).body
54
56
  end
55
57
 
@@ -57,11 +59,11 @@ module ActionView
57
59
  @cache_hits ||= {}
58
60
  end
59
61
 
60
- def render_template_to_object(context, options) #:nodoc:
62
+ def render_template_to_object(context, options) # :nodoc:
61
63
  TemplateRenderer.new(@lookup_context).render(context, options)
62
64
  end
63
65
 
64
- def render_partial_to_object(context, options, &block) #:nodoc:
66
+ def render_partial_to_object(context, options, &block) # :nodoc:
65
67
  partial = options[:partial]
66
68
  if String === partial
67
69
  collection = collection_from_options(options)