actionview 6.1.6.1 → 7.0.0.alpha1

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -347
  3. data/MIT-LICENSE +2 -1
  4. data/lib/action_view/base.rb +3 -3
  5. data/lib/action_view/buffers.rb +2 -2
  6. data/lib/action_view/cache_expiry.rb +46 -32
  7. data/lib/action_view/dependency_tracker/erb_tracker.rb +154 -0
  8. data/lib/action_view/dependency_tracker/ripper_tracker.rb +59 -0
  9. data/lib/action_view/dependency_tracker.rb +6 -147
  10. data/lib/action_view/digestor.rb +7 -4
  11. data/lib/action_view/flows.rb +4 -4
  12. data/lib/action_view/gem_version.rb +4 -4
  13. data/lib/action_view/helpers/active_model_helper.rb +1 -1
  14. data/lib/action_view/helpers/asset_tag_helper.rb +84 -29
  15. data/lib/action_view/helpers/asset_url_helper.rb +7 -7
  16. data/lib/action_view/helpers/atom_feed_helper.rb +3 -4
  17. data/lib/action_view/helpers/cache_helper.rb +51 -3
  18. data/lib/action_view/helpers/capture_helper.rb +2 -2
  19. data/lib/action_view/helpers/controller_helper.rb +2 -2
  20. data/lib/action_view/helpers/csp_helper.rb +1 -1
  21. data/lib/action_view/helpers/csrf_helper.rb +1 -1
  22. data/lib/action_view/helpers/date_helper.rb +6 -7
  23. data/lib/action_view/helpers/debug_helper.rb +3 -1
  24. data/lib/action_view/helpers/form_helper.rb +72 -12
  25. data/lib/action_view/helpers/form_options_helper.rb +65 -33
  26. data/lib/action_view/helpers/form_tag_helper.rb +75 -32
  27. data/lib/action_view/helpers/javascript_helper.rb +3 -5
  28. data/lib/action_view/helpers/number_helper.rb +3 -4
  29. data/lib/action_view/helpers/output_safety_helper.rb +2 -2
  30. data/lib/action_view/helpers/rendering_helper.rb +1 -1
  31. data/lib/action_view/helpers/sanitize_helper.rb +2 -2
  32. data/lib/action_view/helpers/tag_helper.rb +25 -44
  33. data/lib/action_view/helpers/tags/base.rb +3 -15
  34. data/lib/action_view/helpers/tags/check_box.rb +2 -2
  35. data/lib/action_view/helpers/tags/collection_select.rb +1 -1
  36. data/lib/action_view/helpers/tags/hidden_field.rb +0 -4
  37. data/lib/action_view/helpers/tags/time_field.rb +10 -1
  38. data/lib/action_view/helpers/tags/weekday_select.rb +27 -0
  39. data/lib/action_view/helpers/tags.rb +3 -2
  40. data/lib/action_view/helpers/text_helper.rb +24 -13
  41. data/lib/action_view/helpers/translation_helper.rb +1 -2
  42. data/lib/action_view/helpers/url_helper.rb +102 -77
  43. data/lib/action_view/helpers.rb +25 -25
  44. data/lib/action_view/lookup_context.rb +33 -52
  45. data/lib/action_view/model_naming.rb +1 -1
  46. data/lib/action_view/path_set.rb +16 -22
  47. data/lib/action_view/railtie.rb +14 -1
  48. data/lib/action_view/render_parser.rb +188 -0
  49. data/lib/action_view/renderer/abstract_renderer.rb +2 -2
  50. data/lib/action_view/renderer/partial_renderer.rb +0 -34
  51. data/lib/action_view/renderer/renderer.rb +4 -4
  52. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -3
  53. data/lib/action_view/renderer/template_renderer.rb +6 -2
  54. data/lib/action_view/rendering.rb +2 -2
  55. data/lib/action_view/ripper_ast_parser.rb +198 -0
  56. data/lib/action_view/routing_url_for.rb +1 -1
  57. data/lib/action_view/template/error.rb +108 -13
  58. data/lib/action_view/template/handlers/erb.rb +6 -0
  59. data/lib/action_view/template/handlers.rb +3 -3
  60. data/lib/action_view/template/html.rb +3 -3
  61. data/lib/action_view/template/inline.rb +3 -3
  62. data/lib/action_view/template/raw_file.rb +3 -3
  63. data/lib/action_view/template/resolver.rb +84 -311
  64. data/lib/action_view/template/text.rb +3 -3
  65. data/lib/action_view/template/types.rb +14 -12
  66. data/lib/action_view/template.rb +10 -1
  67. data/lib/action_view/template_details.rb +66 -0
  68. data/lib/action_view/template_path.rb +64 -0
  69. data/lib/action_view/test_case.rb +6 -2
  70. data/lib/action_view/testing/resolvers.rb +11 -12
  71. data/lib/action_view/unbound_template.rb +33 -7
  72. data/lib/action_view.rb +3 -4
  73. metadata +25 -19
@@ -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,7 +8,7 @@ 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
@@ -44,44 +44,38 @@ module ActionView #:nodoc:
44
44
  METHOD
45
45
  end
46
46
 
47
- def find(*args)
48
- find_all(*args).first || raise(MissingTemplate.new(self, *args))
47
+ def find(path, prefixes, partial, details, details_key, locals)
48
+ find_all(path, prefixes, partial, details, details_key, locals).first ||
49
+ raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
49
50
  end
50
51
 
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?
57
- end
58
-
59
- def find_all_with_query(query) # :nodoc:
60
- paths.each do |resolver|
61
- templates = resolver.find_all_with_query(query)
52
+ def find_all(path, prefixes, partial, details, details_key, locals)
53
+ search_combinations(prefixes) do |resolver, prefix|
54
+ templates = resolver.find_all(path, prefix, partial, details, details_key, locals)
62
55
  return templates unless templates.empty?
63
56
  end
64
-
65
57
  []
66
58
  end
67
59
 
60
+ def exists?(path, prefixes, partial, details, details_key, locals)
61
+ find_all(path, prefixes, partial, details, details_key, locals).any?
62
+ end
63
+
68
64
  private
69
- def _find_all(path, prefixes, args)
70
- prefixes = [prefixes] if String === prefixes
65
+ def search_combinations(prefixes)
66
+ prefixes = Array(prefixes)
71
67
  prefixes.each do |prefix|
72
68
  paths.each do |resolver|
73
- templates = resolver.find_all(path, prefix, *args)
74
- return templates unless templates.empty?
69
+ yield resolver, prefix
75
70
  end
76
71
  end
77
- []
78
72
  end
79
73
 
80
74
  def typecast(paths)
81
75
  paths.map do |path|
82
76
  case path
83
77
  when Pathname, String
84
- OptimizedFileSystemResolver.new path.to_s
78
+ FileSystemResolver.new path.to_s
85
79
  else
86
80
  path
87
81
  end
@@ -10,6 +10,9 @@ 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
13
16
 
14
17
  config.eager_load_namespaces << ActionView
15
18
 
@@ -38,7 +41,17 @@ module ActionView
38
41
  end
39
42
 
40
43
  config.after_initialize do |app|
44
+ button_to_generates_button_tag = app.config.action_view.delete(:button_to_generates_button_tag)
45
+ unless button_to_generates_button_tag.nil?
46
+ ActionView::Helpers::UrlHelper.button_to_generates_button_tag = button_to_generates_button_tag
47
+ end
48
+ end
49
+
50
+ config.after_initialize do |app|
51
+ ActionView::Helpers::AssetTagHelper.image_loading = app.config.action_view.delete(:image_loading)
52
+ ActionView::Helpers::AssetTagHelper.image_decoding = app.config.action_view.delete(:image_decoding)
41
53
  ActionView::Helpers::AssetTagHelper.preload_links_header = app.config.action_view.delete(:preload_links_header)
54
+ ActionView::Helpers::AssetTagHelper.apply_stylesheet_media_default = app.config.action_view.delete(:apply_stylesheet_media_default)
42
55
  end
43
56
 
44
57
  config.after_initialize do |app|
@@ -85,7 +98,7 @@ module ActionView
85
98
  end
86
99
 
87
100
  unless enable_caching
88
- app.executor.to_run ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
101
+ app.executor.register_hook ActionView::CacheExpiry::Executor.new(watcher: app.config.file_watcher)
89
102
  end
90
103
  end
91
104
 
@@ -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)
@@ -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
@@ -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
 
@@ -44,12 +44,12 @@ module ActionView
44
44
  end
45
45
 
46
46
  # Direct access to template rendering.
47
- def render_template(context, options) #:nodoc:
47
+ def render_template(context, options) # :nodoc:
48
48
  render_template_to_object(context, options).body
49
49
  end
50
50
 
51
51
  # Direct access to partial rendering.
52
- def render_partial(context, options, &block) #:nodoc:
52
+ def render_partial(context, options, &block) # :nodoc:
53
53
  render_partial_to_object(context, options, &block).body
54
54
  end
55
55
 
@@ -57,11 +57,11 @@ module ActionView
57
57
  @cache_hits ||= {}
58
58
  end
59
59
 
60
- def render_template_to_object(context, options) #:nodoc:
60
+ def render_template_to_object(context, options) # :nodoc:
61
61
  TemplateRenderer.new(@lookup_context).render(context, options)
62
62
  end
63
63
 
64
- def render_partial_to_object(context, options, &block) #:nodoc:
64
+ def render_partial_to_object(context, options, &block) # :nodoc:
65
65
  partial = options[:partial]
66
66
  if String === partial
67
67
  collection = collection_from_options(options)
@@ -7,11 +7,11 @@ module ActionView
7
7
  #
8
8
  # * Support streaming from child templates, partials and so on.
9
9
  # * Rack::Cache needs to support streaming bodies
10
- class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
10
+ class StreamingTemplateRenderer < TemplateRenderer # :nodoc:
11
11
  # A valid Rack::Body (i.e. it responds to each).
12
12
  # It is initialized with a block that, when called, starts
13
13
  # rendering the template.
14
- class Body #:nodoc:
14
+ class Body # :nodoc:
15
15
  def initialize(&start)
16
16
  @start = start
17
17
  end
@@ -42,7 +42,7 @@ module ActionView
42
42
  # For streaming, instead of rendering a given a template, we return a Body
43
43
  # object that responds to each. This object is initialized with a block
44
44
  # that knows how to render the template.
45
- def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
45
+ def render_template(view, template, layout_name = nil, locals = {}) # :nodoc:
46
46
  return [super.body] unless layout_name && template.supports_streaming?
47
47
 
48
48
  locals ||= {}
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionView
4
- class TemplateRenderer < AbstractRenderer #:nodoc:
4
+ class TemplateRenderer < AbstractRenderer # :nodoc:
5
5
  def render(context, options)
6
6
  @details = extract_details(options)
7
7
  template = determine_template(options)
@@ -26,7 +26,11 @@ module ActionView
26
26
  if File.exist?(options[:file])
27
27
  Template::RawFile.new(options[:file])
28
28
  else
29
- raise ArgumentError, "`render file:` should be given the absolute path to a file. '#{options[:file]}' was given instead"
29
+ if Pathname.new(options[:file]).absolute?
30
+ raise ArgumentError, "File #{options[:file]} does not exist"
31
+ else
32
+ raise ArgumentError, "`render file:` should be given the absolute path to a file. '#{options[:file]}' was given instead"
33
+ end
30
34
  end
31
35
  elsif options.key?(:inline)
32
36
  handler = Template.handler_for_extension(options[:type] || "erb")
@@ -5,7 +5,7 @@ require "action_view/view_paths"
5
5
  module ActionView
6
6
  # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
7
7
  # it will trigger the lookup_context and consequently expire the cache.
8
- class I18nProxy < ::I18n::Config #:nodoc:
8
+ class I18nProxy < ::I18n::Config # :nodoc:
9
9
  attr_reader :original_config, :lookup_context
10
10
 
11
11
  def initialize(original_config, lookup_context)
@@ -34,7 +34,7 @@ module ActionView
34
34
  end
35
35
 
36
36
  # Overwrite process to set up I18n proxy.
37
- def process(*) #:nodoc:
37
+ def process(*) # :nodoc:
38
38
  old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
39
39
  super
40
40
  ensure
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module ActionView
6
+ class RenderParser
7
+ module RipperASTParser # :nodoc:
8
+ class Node < ::Array # :nodoc:
9
+ attr_reader :type
10
+
11
+ def initialize(type, arr, opts = {})
12
+ @type = type
13
+ super(arr)
14
+ end
15
+
16
+ def children
17
+ to_a
18
+ end
19
+
20
+ def inspect
21
+ typeinfo = type && type != :list ? ":" + type.to_s + ", " : ""
22
+ "s(" + typeinfo + map(&:inspect).join(", ") + ")"
23
+ end
24
+
25
+ def fcall?
26
+ type == :command || type == :fcall
27
+ end
28
+
29
+ def fcall_named?(name)
30
+ fcall? &&
31
+ self[0].type == :@ident &&
32
+ self[0][0] == name
33
+ end
34
+
35
+ def argument_nodes
36
+ raise unless fcall?
37
+ return [] if self[1].nil?
38
+ if self[1].last == false || self[1].last.type == :vcall
39
+ self[1][0...-1]
40
+ else
41
+ self[1][0..-1]
42
+ end
43
+ end
44
+
45
+ def string?
46
+ type == :string_literal
47
+ end
48
+
49
+ def variable_reference?
50
+ type == :var_ref
51
+ end
52
+
53
+ def vcall?
54
+ type == :vcall
55
+ end
56
+
57
+ def call?
58
+ type == :call
59
+ end
60
+
61
+ def variable_name
62
+ self[0][0]
63
+ end
64
+
65
+ def call_method_name
66
+ self.last.first
67
+ end
68
+
69
+ def to_string
70
+ raise unless string?
71
+ self[0][0][0]
72
+ end
73
+
74
+ def hash?
75
+ type == :bare_assoc_hash || type == :hash
76
+ end
77
+
78
+ def to_hash
79
+ if type == :bare_assoc_hash
80
+ hash_from_body(self[0])
81
+ elsif type == :hash && self[0] == nil
82
+ {}
83
+ elsif type == :hash && self[0].type == :assoclist_from_args
84
+ hash_from_body(self[0][0])
85
+ end
86
+ end
87
+
88
+ def hash_from_body(body)
89
+ body.map do |hash_node|
90
+ return nil if hash_node.type != :assoc_new
91
+
92
+ [hash_node[0], hash_node[1]]
93
+ end.to_h
94
+ end
95
+
96
+ def symbol?
97
+ type == :@label || type == :symbol_literal
98
+ end
99
+
100
+ def to_symbol
101
+ if type == :@label && self[0] =~ /\A(.+):\z/
102
+ $1.to_sym
103
+ elsif type == :symbol_literal && self[0].type == :symbol && self[0][0].type == :@ident
104
+ self[0][0][0].to_sym
105
+ else
106
+ raise "not a symbol?: #{self.inspect}"
107
+ end
108
+ end
109
+ end
110
+
111
+ class NodeParser < ::Ripper # :nodoc:
112
+ PARSER_EVENTS.each do |event|
113
+ arity = PARSER_EVENT_TABLE[event]
114
+ if arity == 0 && event.to_s.end_with?("_new")
115
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
116
+ def on_#{event}(*args)
117
+ Node.new(:list, args, lineno: lineno(), column: column())
118
+ end
119
+ eof
120
+ elsif event.to_s.match?(/_add(_.+)?\z/)
121
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
122
+ begin; undef on_#{event}; rescue NameError; end
123
+ def on_#{event}(list, item)
124
+ list.push(item)
125
+ list
126
+ end
127
+ eof
128
+ else
129
+ module_eval(<<-eof, __FILE__, __LINE__ + 1)
130
+ begin; undef on_#{event}; rescue NameError; end
131
+ def on_#{event}(*args)
132
+ Node.new(:#{event}, args, lineno: lineno(), column: column())
133
+ end
134
+ eof
135
+ end
136
+ end
137
+
138
+ SCANNER_EVENTS.each do |event|
139
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
140
+ def on_#{event}(tok)
141
+ Node.new(:@#{event}, [tok], lineno: lineno(), column: column())
142
+ end
143
+ End
144
+ end
145
+ end
146
+
147
+ class RenderCallExtractor < NodeParser # :nodoc:
148
+ attr_reader :render_calls
149
+
150
+ METHODS_TO_PARSE = %w(render render_to_string)
151
+
152
+ def initialize(*args)
153
+ super
154
+
155
+ @render_calls = []
156
+ end
157
+
158
+ private
159
+ def on_fcall(name, *args)
160
+ on_render_call(super)
161
+ end
162
+
163
+ def on_command(name, *args)
164
+ on_render_call(super)
165
+ end
166
+
167
+ def on_render_call(node)
168
+ METHODS_TO_PARSE.each do |method|
169
+ if node.fcall_named?(method)
170
+ @render_calls << [method, node]
171
+ return node
172
+ end
173
+ end
174
+ node
175
+ end
176
+
177
+ def on_arg_paren(content)
178
+ content
179
+ end
180
+
181
+ def on_paren(content)
182
+ content
183
+ end
184
+ end
185
+
186
+ extend self
187
+
188
+ def parse_render_nodes(code)
189
+ parser = RenderCallExtractor.new(code)
190
+ parser.parse
191
+
192
+ parser.render_calls.group_by(&:first).collect do |method, nodes|
193
+ [ method.to_sym, nodes.collect { |v| v[1] } ]
194
+ end.to_h
195
+ end
196
+ end
197
+ end
198
+ end
@@ -118,7 +118,7 @@ module ActionView
118
118
  end
119
119
  end
120
120
 
121
- def url_options #:nodoc:
121
+ def url_options # :nodoc:
122
122
  return super unless controller.respond_to?(:url_options)
123
123
  controller.url_options
124
124
  end