omg-actionview 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +25 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +40 -0
  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 +316 -0
  8. data/lib/action_view/buffers.rb +165 -0
  9. data/lib/action_view/cache_expiry.rb +69 -0
  10. data/lib/action_view/context.rb +32 -0
  11. data/lib/action_view/dependency_tracker/erb_tracker.rb +159 -0
  12. data/lib/action_view/dependency_tracker/ruby_tracker.rb +43 -0
  13. data/lib/action_view/dependency_tracker/wildcard_resolver.rb +32 -0
  14. data/lib/action_view/dependency_tracker.rb +41 -0
  15. data/lib/action_view/deprecator.rb +7 -0
  16. data/lib/action_view/digestor.rb +130 -0
  17. data/lib/action_view/flows.rb +75 -0
  18. data/lib/action_view/gem_version.rb +17 -0
  19. data/lib/action_view/helpers/active_model_helper.rb +54 -0
  20. data/lib/action_view/helpers/asset_tag_helper.rb +680 -0
  21. data/lib/action_view/helpers/asset_url_helper.rb +473 -0
  22. data/lib/action_view/helpers/atom_feed_helper.rb +205 -0
  23. data/lib/action_view/helpers/cache_helper.rb +315 -0
  24. data/lib/action_view/helpers/capture_helper.rb +236 -0
  25. data/lib/action_view/helpers/content_exfiltration_prevention_helper.rb +70 -0
  26. data/lib/action_view/helpers/controller_helper.rb +42 -0
  27. data/lib/action_view/helpers/csp_helper.rb +26 -0
  28. data/lib/action_view/helpers/csrf_helper.rb +35 -0
  29. data/lib/action_view/helpers/date_helper.rb +1266 -0
  30. data/lib/action_view/helpers/debug_helper.rb +38 -0
  31. data/lib/action_view/helpers/form_helper.rb +2765 -0
  32. data/lib/action_view/helpers/form_options_helper.rb +927 -0
  33. data/lib/action_view/helpers/form_tag_helper.rb +1088 -0
  34. data/lib/action_view/helpers/javascript_helper.rb +96 -0
  35. data/lib/action_view/helpers/number_helper.rb +165 -0
  36. data/lib/action_view/helpers/output_safety_helper.rb +70 -0
  37. data/lib/action_view/helpers/rendering_helper.rb +218 -0
  38. data/lib/action_view/helpers/sanitize_helper.rb +201 -0
  39. data/lib/action_view/helpers/tag_helper.rb +621 -0
  40. data/lib/action_view/helpers/tags/base.rb +138 -0
  41. data/lib/action_view/helpers/tags/check_box.rb +65 -0
  42. data/lib/action_view/helpers/tags/checkable.rb +18 -0
  43. data/lib/action_view/helpers/tags/collection_check_boxes.rb +37 -0
  44. data/lib/action_view/helpers/tags/collection_helpers.rb +118 -0
  45. data/lib/action_view/helpers/tags/collection_radio_buttons.rb +31 -0
  46. data/lib/action_view/helpers/tags/collection_select.rb +33 -0
  47. data/lib/action_view/helpers/tags/color_field.rb +26 -0
  48. data/lib/action_view/helpers/tags/date_field.rb +14 -0
  49. data/lib/action_view/helpers/tags/date_select.rb +75 -0
  50. data/lib/action_view/helpers/tags/datetime_field.rb +39 -0
  51. data/lib/action_view/helpers/tags/datetime_local_field.rb +29 -0
  52. data/lib/action_view/helpers/tags/datetime_select.rb +10 -0
  53. data/lib/action_view/helpers/tags/email_field.rb +10 -0
  54. data/lib/action_view/helpers/tags/file_field.rb +26 -0
  55. data/lib/action_view/helpers/tags/grouped_collection_select.rb +34 -0
  56. data/lib/action_view/helpers/tags/hidden_field.rb +14 -0
  57. data/lib/action_view/helpers/tags/label.rb +84 -0
  58. data/lib/action_view/helpers/tags/month_field.rb +14 -0
  59. data/lib/action_view/helpers/tags/number_field.rb +20 -0
  60. data/lib/action_view/helpers/tags/password_field.rb +14 -0
  61. data/lib/action_view/helpers/tags/placeholderable.rb +24 -0
  62. data/lib/action_view/helpers/tags/radio_button.rb +32 -0
  63. data/lib/action_view/helpers/tags/range_field.rb +10 -0
  64. data/lib/action_view/helpers/tags/search_field.rb +27 -0
  65. data/lib/action_view/helpers/tags/select.rb +45 -0
  66. data/lib/action_view/helpers/tags/select_renderer.rb +56 -0
  67. data/lib/action_view/helpers/tags/tel_field.rb +10 -0
  68. data/lib/action_view/helpers/tags/text_area.rb +24 -0
  69. data/lib/action_view/helpers/tags/text_field.rb +33 -0
  70. data/lib/action_view/helpers/tags/time_field.rb +23 -0
  71. data/lib/action_view/helpers/tags/time_select.rb +10 -0
  72. data/lib/action_view/helpers/tags/time_zone_select.rb +25 -0
  73. data/lib/action_view/helpers/tags/translator.rb +39 -0
  74. data/lib/action_view/helpers/tags/url_field.rb +10 -0
  75. data/lib/action_view/helpers/tags/week_field.rb +14 -0
  76. data/lib/action_view/helpers/tags/weekday_select.rb +31 -0
  77. data/lib/action_view/helpers/tags.rb +47 -0
  78. data/lib/action_view/helpers/text_helper.rb +568 -0
  79. data/lib/action_view/helpers/translation_helper.rb +161 -0
  80. data/lib/action_view/helpers/url_helper.rb +812 -0
  81. data/lib/action_view/helpers.rb +68 -0
  82. data/lib/action_view/layouts.rb +434 -0
  83. data/lib/action_view/locale/en.yml +56 -0
  84. data/lib/action_view/log_subscriber.rb +132 -0
  85. data/lib/action_view/lookup_context.rb +299 -0
  86. data/lib/action_view/model_naming.rb +14 -0
  87. data/lib/action_view/path_registry.rb +57 -0
  88. data/lib/action_view/path_set.rb +84 -0
  89. data/lib/action_view/railtie.rb +132 -0
  90. data/lib/action_view/record_identifier.rb +118 -0
  91. data/lib/action_view/render_parser/prism_render_parser.rb +139 -0
  92. data/lib/action_view/render_parser/ripper_render_parser.rb +350 -0
  93. data/lib/action_view/render_parser.rb +40 -0
  94. data/lib/action_view/renderer/abstract_renderer.rb +186 -0
  95. data/lib/action_view/renderer/collection_renderer.rb +204 -0
  96. data/lib/action_view/renderer/object_renderer.rb +34 -0
  97. data/lib/action_view/renderer/partial_renderer/collection_caching.rb +120 -0
  98. data/lib/action_view/renderer/partial_renderer.rb +267 -0
  99. data/lib/action_view/renderer/renderer.rb +107 -0
  100. data/lib/action_view/renderer/streaming_template_renderer.rb +107 -0
  101. data/lib/action_view/renderer/template_renderer.rb +115 -0
  102. data/lib/action_view/rendering.rb +190 -0
  103. data/lib/action_view/routing_url_for.rb +149 -0
  104. data/lib/action_view/tasks/cache_digests.rake +25 -0
  105. data/lib/action_view/template/error.rb +264 -0
  106. data/lib/action_view/template/handlers/builder.rb +25 -0
  107. data/lib/action_view/template/handlers/erb/erubi.rb +85 -0
  108. data/lib/action_view/template/handlers/erb.rb +157 -0
  109. data/lib/action_view/template/handlers/html.rb +11 -0
  110. data/lib/action_view/template/handlers/raw.rb +11 -0
  111. data/lib/action_view/template/handlers.rb +66 -0
  112. data/lib/action_view/template/html.rb +33 -0
  113. data/lib/action_view/template/inline.rb +22 -0
  114. data/lib/action_view/template/raw_file.rb +25 -0
  115. data/lib/action_view/template/renderable.rb +30 -0
  116. data/lib/action_view/template/resolver.rb +212 -0
  117. data/lib/action_view/template/sources/file.rb +17 -0
  118. data/lib/action_view/template/sources.rb +13 -0
  119. data/lib/action_view/template/text.rb +32 -0
  120. data/lib/action_view/template/types.rb +50 -0
  121. data/lib/action_view/template.rb +580 -0
  122. data/lib/action_view/template_details.rb +66 -0
  123. data/lib/action_view/template_path.rb +66 -0
  124. data/lib/action_view/test_case.rb +449 -0
  125. data/lib/action_view/testing/resolvers.rb +44 -0
  126. data/lib/action_view/unbound_template.rb +67 -0
  127. data/lib/action_view/version.rb +10 -0
  128. data/lib/action_view/view_paths.rb +117 -0
  129. data/lib/action_view.rb +104 -0
  130. metadata +275 -0
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module RenderParser
5
+ class PrismRenderParser < Base # :nodoc:
6
+ def render_calls
7
+ queue = [Prism.parse(@code).value]
8
+ templates = []
9
+
10
+ while (node = queue.shift)
11
+ queue.concat(node.compact_child_nodes)
12
+ next unless node.is_a?(Prism::CallNode)
13
+
14
+ options = render_call_options(node)
15
+ next unless options
16
+
17
+ render_type = (options.keys & RENDER_TYPE_KEYS)[0]
18
+ template, object_template = render_call_template(options[render_type])
19
+ next unless template
20
+
21
+ if options.key?(:object) || options.key?(:collection) || object_template
22
+ next if options.key?(:object) && options.key?(:collection)
23
+ next unless options.key?(:partial)
24
+ end
25
+
26
+ if options[:spacer_template].is_a?(Prism::StringNode)
27
+ templates << partial_to_virtual_path(:partial, options[:spacer_template].unescaped)
28
+ end
29
+
30
+ templates << partial_to_virtual_path(render_type, template)
31
+
32
+ if render_type != :layout && options[:layout].is_a?(Prism::StringNode)
33
+ templates << partial_to_virtual_path(:layout, options[:layout].unescaped)
34
+ end
35
+ end
36
+
37
+ templates
38
+ end
39
+
40
+ private
41
+ # Accept a call node and return a hash of options for the render call.
42
+ # If it doesn't match the expected format, return nil.
43
+ def render_call_options(node)
44
+ # We are only looking for calls to render or render_to_string.
45
+ name = node.name.to_sym
46
+ return if name != :render && name != :render_to_string
47
+
48
+ # We are only looking for calls with arguments.
49
+ arguments = node.arguments
50
+ return unless arguments
51
+
52
+ arguments = arguments.arguments
53
+ length = arguments.length
54
+
55
+ # Get rid of any parentheses to get directly to the contents.
56
+ arguments.map! do |argument|
57
+ current = argument
58
+
59
+ while current.is_a?(Prism::ParenthesesNode) &&
60
+ current.body.is_a?(Prism::StatementsNode) &&
61
+ current.body.body.length == 1
62
+ current = current.body.body.first
63
+ end
64
+
65
+ current
66
+ end
67
+
68
+ # We are only looking for arguments that are either a string with an
69
+ # array of locals or a keyword hash with symbol keys.
70
+ options =
71
+ if (length == 1 || length == 2) && !arguments[0].is_a?(Prism::KeywordHashNode)
72
+ { partial: arguments[0], locals: arguments[1] }
73
+ elsif length == 1 &&
74
+ arguments[0].is_a?(Prism::KeywordHashNode) &&
75
+ arguments[0].elements.all? do |element|
76
+ element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode)
77
+ end
78
+ arguments[0].elements.to_h do |element|
79
+ [element.key.unescaped.to_sym, element.value]
80
+ end
81
+ end
82
+
83
+ return unless options
84
+
85
+ # Here we validate that the options have the keys we expect.
86
+ keys = options.keys
87
+ return if !keys.intersect?(RENDER_TYPE_KEYS)
88
+ return if (keys - ALL_KNOWN_KEYS).any?
89
+
90
+ # Finally, we can return a valid set of options.
91
+ options
92
+ end
93
+
94
+ # Accept the node that is being passed in the position of the template
95
+ # and return the template name and whether or not it is an object
96
+ # template.
97
+ def render_call_template(node)
98
+ object_template = false
99
+ template =
100
+ case node.type
101
+ when :string_node
102
+ path = node.unescaped
103
+ path.include?("/") ? path : "#{directory}/#{path}"
104
+ when :interpolated_string_node
105
+ node.parts.map do |node|
106
+ case node.type
107
+ when :string_node
108
+ node.unescaped
109
+ when :embedded_statements_node
110
+ "*"
111
+ else
112
+ return
113
+ end
114
+ end.join("")
115
+ else
116
+ dependency =
117
+ case node.type
118
+ when :class_variable_read_node
119
+ node.slice[2..]
120
+ when :instance_variable_read_node
121
+ node.slice[1..]
122
+ when :global_variable_read_node
123
+ node.slice[1..]
124
+ when :local_variable_read_node
125
+ node.slice
126
+ when :call_node
127
+ node.name.to_s
128
+ else
129
+ return
130
+ end
131
+
132
+ "#{dependency.pluralize}/#{dependency.singularize}"
133
+ end
134
+
135
+ [template, object_template]
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,350 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module RenderParser
5
+ class RipperRenderParser < Base # :nodoc:
6
+ class Node < ::Array # :nodoc:
7
+ attr_reader :type
8
+
9
+ def initialize(type, arr, opts = {})
10
+ @type = type
11
+ super(arr)
12
+ end
13
+
14
+ def children
15
+ to_a
16
+ end
17
+
18
+ def inspect
19
+ typeinfo = type && type != :list ? ":" + type.to_s + ", " : ""
20
+ "s(" + typeinfo + map(&:inspect).join(", ") + ")"
21
+ end
22
+
23
+ def fcall?
24
+ type == :command || type == :fcall
25
+ end
26
+
27
+ def fcall_named?(name)
28
+ fcall? &&
29
+ self[0].type == :@ident &&
30
+ self[0][0] == name
31
+ end
32
+
33
+ def argument_nodes
34
+ raise unless fcall?
35
+ return [] if self[1].nil?
36
+ if self[1].last == false || self[1].last.type == :vcall
37
+ self[1][0...-1]
38
+ else
39
+ self[1][0..-1]
40
+ end
41
+ end
42
+
43
+ def string?
44
+ type == :string_literal
45
+ end
46
+
47
+ def variable_reference?
48
+ type == :var_ref
49
+ end
50
+
51
+ def vcall?
52
+ type == :vcall
53
+ end
54
+
55
+ def call?
56
+ type == :call
57
+ end
58
+
59
+ def variable_name
60
+ self[0][0]
61
+ end
62
+
63
+ def call_method_name
64
+ self[2].first
65
+ end
66
+
67
+ def to_string
68
+ raise unless string?
69
+
70
+ # s(:string_literal, s(:string_content, map))
71
+ self[0].map do |node|
72
+ case node.type
73
+ when :@tstring_content
74
+ node[0]
75
+ when :string_embexpr
76
+ "*"
77
+ end
78
+ end.join("")
79
+ end
80
+
81
+ def hash?
82
+ type == :bare_assoc_hash || type == :hash
83
+ end
84
+
85
+ def to_hash
86
+ if type == :bare_assoc_hash
87
+ hash_from_body(self[0])
88
+ elsif type == :hash && self[0] == nil
89
+ {}
90
+ elsif type == :hash && self[0].type == :assoclist_from_args
91
+ hash_from_body(self[0][0])
92
+ end
93
+ end
94
+
95
+ def hash_from_body(body)
96
+ body.to_h do |hash_node|
97
+ return nil if hash_node.type != :assoc_new
98
+
99
+ [hash_node[0], hash_node[1]]
100
+ end
101
+ end
102
+
103
+ def symbol?
104
+ type == :@label || type == :symbol_literal
105
+ end
106
+
107
+ def to_symbol
108
+ if type == :@label && self[0] =~ /\A(.+):\z/
109
+ $1.to_sym
110
+ elsif type == :symbol_literal && self[0].type == :symbol && self[0][0].type == :@ident
111
+ self[0][0][0].to_sym
112
+ else
113
+ raise "not a symbol?: #{self.inspect}"
114
+ end
115
+ end
116
+ end
117
+
118
+ class NodeParser < ::Ripper # :nodoc:
119
+ PARSER_EVENTS.each do |event|
120
+ arity = PARSER_EVENT_TABLE[event]
121
+ if arity == 0 && event.to_s.end_with?("_new")
122
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
123
+ def on_#{event}(*args)
124
+ Node.new(:list, args, lineno: lineno(), column: column())
125
+ end
126
+ eof
127
+ elsif event.to_s.match?(/_add(_.+)?\z/)
128
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
129
+ begin; undef on_#{event}; rescue NameError; end
130
+ def on_#{event}(list, item)
131
+ list.push(item)
132
+ list
133
+ end
134
+ eof
135
+ else
136
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
137
+ begin; undef on_#{event}; rescue NameError; end
138
+ def on_#{event}(*args)
139
+ Node.new(:#{event}, args, lineno: lineno(), column: column())
140
+ end
141
+ eof
142
+ end
143
+ end
144
+
145
+ SCANNER_EVENTS.each do |event|
146
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
147
+ def on_#{event}(tok)
148
+ Node.new(:@#{event}, [tok], lineno: lineno(), column: column())
149
+ end
150
+ End
151
+ end
152
+ end
153
+
154
+ class RenderCallExtractor < NodeParser # :nodoc:
155
+ attr_reader :render_calls
156
+
157
+ METHODS_TO_PARSE = %w(render render_to_string)
158
+
159
+ def initialize(*args)
160
+ super
161
+
162
+ @render_calls = []
163
+ end
164
+
165
+ private
166
+ def on_fcall(name, *args)
167
+ on_render_call(super)
168
+ end
169
+
170
+ def on_command(name, *args)
171
+ on_render_call(super)
172
+ end
173
+
174
+ def on_render_call(node)
175
+ METHODS_TO_PARSE.each do |method|
176
+ if node.fcall_named?(method)
177
+ @render_calls << [method, node]
178
+ return node
179
+ end
180
+ end
181
+ node
182
+ end
183
+
184
+ def on_arg_paren(content)
185
+ content
186
+ end
187
+
188
+ def on_paren(content)
189
+ content.size == 1 ? content.first : content
190
+ end
191
+ end
192
+
193
+ def render_calls
194
+ parser = RenderCallExtractor.new(@code)
195
+ parser.parse
196
+
197
+ parser.render_calls.group_by(&:first).to_h do |method, nodes|
198
+ [ method.to_sym, nodes.collect { |v| v[1] } ]
199
+ end.map do |method, nodes|
200
+ nodes.map { |n| parse_render(n) }
201
+ end.flatten.compact
202
+ end
203
+
204
+ private
205
+ def resolve_path_directory(path)
206
+ if path.include?("/")
207
+ path
208
+ else
209
+ "#{directory}/#{path}"
210
+ end
211
+ end
212
+
213
+ # Convert
214
+ # render("foo", ...)
215
+ # into either
216
+ # render(template: "foo", ...)
217
+ # or
218
+ # render(partial: "foo", ...)
219
+ def normalize_args(string, options_hash)
220
+ if options_hash
221
+ { partial: string, locals: options_hash }
222
+ else
223
+ { partial: string }
224
+ end
225
+ end
226
+
227
+ def parse_render(node)
228
+ node = node.argument_nodes
229
+
230
+ if (node.length == 1 || node.length == 2) && !node[0].hash?
231
+ if node.length == 1
232
+ options = normalize_args(node[0], nil)
233
+ elsif node.length == 2
234
+ options = normalize_args(node[0], node[1])
235
+ end
236
+
237
+ return nil unless options
238
+
239
+ parse_render_from_options(options)
240
+ elsif node.length == 1 && node[0].hash?
241
+ options = parse_hash_to_symbols(node[0])
242
+
243
+ return nil unless options
244
+
245
+ parse_render_from_options(options)
246
+ else
247
+ nil
248
+ end
249
+ end
250
+
251
+ def parse_hash(node)
252
+ node.hash? && node.to_hash
253
+ end
254
+
255
+ def parse_hash_to_symbols(node)
256
+ hash = parse_hash(node)
257
+
258
+ return unless hash
259
+
260
+ hash.transform_keys do |key_node|
261
+ key = parse_sym(key_node)
262
+
263
+ return unless key
264
+
265
+ key
266
+ end
267
+ end
268
+
269
+ def parse_render_from_options(options_hash)
270
+ renders = []
271
+ keys = options_hash.keys
272
+
273
+ if (keys & RENDER_TYPE_KEYS).size < 1
274
+ # Must have at least one of render keys
275
+ return nil
276
+ end
277
+
278
+ if (keys - ALL_KNOWN_KEYS).any?
279
+ # de-opt in case of unknown option
280
+ return nil
281
+ end
282
+
283
+ render_type = (keys & RENDER_TYPE_KEYS)[0]
284
+
285
+ node = options_hash[render_type]
286
+
287
+ if node.string?
288
+ template = resolve_path_directory(node.to_string)
289
+ else
290
+ if node.variable_reference?
291
+ dependency = node.variable_name.sub(/\A(?:\$|@{1,2})/, "")
292
+ elsif node.vcall?
293
+ dependency = node.variable_name
294
+ elsif node.call?
295
+ dependency = node.call_method_name
296
+ else
297
+ return
298
+ end
299
+
300
+ object_template = true
301
+ template = "#{dependency.pluralize}/#{dependency.singularize}"
302
+ end
303
+
304
+ return unless template
305
+
306
+ if spacer_template = render_template_with_spacer?(options_hash)
307
+ virtual_path = partial_to_virtual_path(:partial, spacer_template)
308
+ renders << virtual_path
309
+ end
310
+
311
+ if options_hash.key?(:object) || options_hash.key?(:collection) || object_template
312
+ return nil if options_hash.key?(:object) && options_hash.key?(:collection)
313
+ return nil unless options_hash.key?(:partial)
314
+ end
315
+
316
+ virtual_path = partial_to_virtual_path(render_type, template)
317
+ renders << virtual_path
318
+
319
+ # Support for rendering multiple templates (i.e. a partial with a layout)
320
+ if layout_template = render_template_with_layout?(render_type, options_hash)
321
+ virtual_path = partial_to_virtual_path(:layout, layout_template)
322
+
323
+ renders << virtual_path
324
+ end
325
+
326
+ renders
327
+ end
328
+
329
+ def parse_str(node)
330
+ node.string? && node.to_string
331
+ end
332
+
333
+ def parse_sym(node)
334
+ node.symbol? && node.to_symbol
335
+ end
336
+
337
+ def render_template_with_layout?(render_type, options_hash)
338
+ if render_type != :layout && options_hash.key?(:layout)
339
+ parse_str(options_hash[:layout])
340
+ end
341
+ end
342
+
343
+ def render_template_with_spacer?(options_hash)
344
+ if options_hash.key?(:spacer_template)
345
+ parse_str(options_hash[:spacer_template])
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionView
4
+ module RenderParser # :nodoc:
5
+ ALL_KNOWN_KEYS = [:partial, :template, :layout, :formats, :locals, :object, :collection, :as, :status, :content_type, :location, :spacer_template]
6
+ RENDER_TYPE_KEYS = [:partial, :template, :layout]
7
+
8
+ class Base # :nodoc:
9
+ def initialize(name, code)
10
+ @name = name
11
+ @code = code
12
+ end
13
+
14
+ private
15
+ def directory
16
+ File.dirname(@name)
17
+ end
18
+
19
+ def partial_to_virtual_path(render_type, partial_path)
20
+ if render_type == :partial || render_type == :layout
21
+ partial_path.gsub(%r{(/|^)([^/]*)\z}, '\1_\2')
22
+ else
23
+ partial_path
24
+ end
25
+ end
26
+ end
27
+
28
+ # Check if prism is available. If it is, use it. Otherwise, use ripper.
29
+ begin
30
+ require "prism"
31
+ rescue LoadError
32
+ require "ripper"
33
+ require_relative "render_parser/ripper_render_parser"
34
+ Default = RipperRenderParser
35
+ else
36
+ require_relative "render_parser/prism_render_parser"
37
+ Default = PrismRenderParser
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ module ActionView
6
+ # This class defines the interface for a renderer. Each class that
7
+ # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
8
+ # render a specific type of object.
9
+ #
10
+ # The base +Renderer+ class uses its +render+ method to delegate to the
11
+ # renderers. These currently consist of
12
+ #
13
+ # PartialRenderer - Used for rendering partials
14
+ # TemplateRenderer - Used for rendering other types of templates
15
+ # StreamingTemplateRenderer - Used for streaming
16
+ #
17
+ # Whenever the +render+ method is called on the base +Renderer+ class, a new
18
+ # renderer object of the correct type is created, and the +render+ method on
19
+ # that new object is called in turn. This abstracts the set up and rendering
20
+ # into a separate classes for partials and templates.
21
+ class AbstractRenderer # :nodoc:
22
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
23
+
24
+ def initialize(lookup_context)
25
+ @lookup_context = lookup_context
26
+ end
27
+
28
+ def render
29
+ raise NotImplementedError
30
+ end
31
+
32
+ module ObjectRendering # :nodoc:
33
+ PREFIXED_PARTIAL_NAMES = Concurrent::Map.new do |h, k|
34
+ h.compute_if_absent(k) { Concurrent::Map.new }
35
+ end
36
+
37
+ def initialize(lookup_context, options)
38
+ super
39
+ @context_prefix = lookup_context.prefixes.first
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
108
+ end
109
+
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
139
+ end
140
+
141
+ class RenderedTemplate # :nodoc:
142
+ attr_reader :body, :template
143
+
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
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
185
+ end
186
+ end