actionview 6.1.7.2 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
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)