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
@@ -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,11 +42,11 @@ 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 ||= {}
49
- layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
49
+ layout = find_layout(layout_name, locals.keys, [formats.first])
50
50
 
51
51
  Body.new do |buffer|
52
52
  delayed_render(buffer, template, layout, view, locals)
@@ -65,7 +65,8 @@ module ActionView
65
65
  ActiveSupport::Notifications.instrument(
66
66
  "render_template.action_view",
67
67
  identifier: template.identifier,
68
- layout: layout && layout.virtual_path
68
+ layout: layout && layout.virtual_path,
69
+ locals: locals
69
70
  ) do
70
71
  outer_config = I18n.config
71
72
  fiber = Fiber.new do
@@ -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")
@@ -45,7 +49,7 @@ module ActionView
45
49
  @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
46
50
  end
47
51
  else
48
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
52
+ raise ArgumentError, "You invoked render but did not give any of :body, :file, :html, :inline, :partial, :plain, :renderable, or :template option."
49
53
  end
50
54
  end
51
55
 
@@ -56,7 +60,8 @@ module ActionView
56
60
  ActiveSupport::Notifications.instrument(
57
61
  "render_template.action_view",
58
62
  identifier: template.identifier,
59
- layout: layout && layout.virtual_path
63
+ layout: layout && layout.virtual_path,
64
+ locals: locals
60
65
  ) do
61
66
  template.render(view, locals) { |*name| view._layout_for(*name) }
62
67
  end
@@ -5,12 +5,13 @@ 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)
12
12
  original_config = original_config.original_config if original_config.respond_to?(:original_config)
13
- @original_config, @lookup_context = original_config, lookup_context
13
+ @original_config = original_config
14
+ @lookup_context = lookup_context
14
15
  end
15
16
 
16
17
  def locale
@@ -33,8 +34,8 @@ module ActionView
33
34
  super
34
35
  end
35
36
 
36
- # Overwrite process to set up I18n proxy.
37
- def process(*) #:nodoc:
37
+ # Override process to set up I18n proxy.
38
+ def process(...) # :nodoc:
38
39
  old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
39
40
  super
40
41
  ensure
@@ -48,7 +49,18 @@ module ActionView
48
49
  def _helpers
49
50
  end
50
51
 
52
+ def inherit_view_context_class?
53
+ superclass.respond_to?(:view_context_class) &&
54
+ supports_path? == superclass.supports_path? &&
55
+ _routes.equal?(superclass._routes) &&
56
+ _helpers.equal?(superclass._helpers)
57
+ end
58
+
51
59
  def build_view_context_class(klass, supports_path, routes, helpers)
60
+ if inherit_view_context_class?
61
+ return superclass.view_context_class
62
+ end
63
+
52
64
  Class.new(klass) do
53
65
  if routes
54
66
  include routes.url_helpers(supports_path)
@@ -61,8 +73,14 @@ module ActionView
61
73
  end
62
74
  end
63
75
 
76
+ def eager_load!
77
+ super
78
+ view_context_class
79
+ nil
80
+ end
81
+
64
82
  def view_context_class
65
- klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
83
+ klass = ActionView::LookupContext::DetailsKey.view_context_class
66
84
 
67
85
  @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
68
86
 
@@ -129,8 +147,8 @@ module ActionView
129
147
  lookup_context.formats = [format.to_sym] if format.to_sym
130
148
  end
131
149
 
132
- # Normalize args by converting render "foo" to render :action => "foo" and
133
- # render "foo/bar" to render :template => "foo/bar".
150
+ # Normalize args by converting render "foo" to render action: "foo" and
151
+ # render "foo/bar" to render template: "foo/bar".
134
152
  def _normalize_args(action = nil, options = {})
135
153
  options = super(action, options)
136
154
  case action
@@ -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[2].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.to_h do |hash_node|
90
+ return nil if hash_node.type != :assoc_new
91
+
92
+ [hash_node[0], hash_node[1]]
93
+ end
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.size == 1 ? content.first : 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).to_h do |method, nodes|
193
+ [ method.to_sym, nodes.collect { |v| v[1] } ]
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -6,14 +6,14 @@ module ActionView
6
6
  module RoutingUrlFor
7
7
  # Returns the URL for the set of +options+ provided. This takes the
8
8
  # same options as +url_for+ in Action Controller (see the
9
- # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
10
- # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
11
- # instead of the fully qualified URL like "http://example.com/controller/action".
9
+ # documentation for ActionDispatch::Routing::UrlFor#url_for). Note that by default
10
+ # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative <tt>"/controller/action"</tt>
11
+ # instead of the fully qualified URL like <tt>"http://example.com/controller/action"</tt>.
12
12
  #
13
13
  # ==== Options
14
14
  # * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
15
15
  # * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
16
- # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
16
+ # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in <tt>"/archive/2005/"</tt>. Note that this
17
17
  # is currently not recommended since it breaks caching.
18
18
  # * <tt>:host</tt> - Overrides the default (current) host if provided.
19
19
  # * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
@@ -47,6 +47,9 @@ module ActionView
47
47
  # <%= url_for(action: 'jump', anchor: 'tax&ship') %>
48
48
  # # => /testing/jump/#tax&ship
49
49
  #
50
+ # <%= url_for(Workshop) %>
51
+ # # => /workshops
52
+ #
50
53
  # <%= url_for(Workshop.new) %>
51
54
  # # relies on Workshop answering a persisted? call (and in this case returning false)
52
55
  # # => /workshops
@@ -118,7 +121,7 @@ module ActionView
118
121
  end
119
122
  end
120
123
 
121
- def url_options #:nodoc:
124
+ def url_options # :nodoc:
122
125
  return super unless controller.respond_to?(:url_options)
123
126
  controller.url_options
124
127
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/enumerable"
4
+ require "active_support/syntax_error_proxy"
4
5
 
5
6
  module ActionView
6
7
  # = Action View Errors
7
- class ActionViewError < StandardError #:nodoc:
8
+ class ActionViewError < StandardError # :nodoc:
8
9
  end
9
10
 
10
- class EncodingError < StandardError #:nodoc:
11
+ class EncodingError < StandardError # :nodoc:
11
12
  end
12
13
 
13
- class WrongEncodingError < EncodingError #:nodoc:
14
+ class WrongEncodingError < EncodingError # :nodoc:
14
15
  def initialize(string, encoding)
15
16
  @string, @encoding = string, encoding
16
17
  end
@@ -26,12 +27,18 @@ module ActionView
26
27
  end
27
28
  end
28
29
 
29
- class MissingTemplate < ActionViewError #:nodoc:
30
- attr_reader :path
30
+ class MissingTemplate < ActionViewError # :nodoc:
31
+ attr_reader :path, :paths, :prefixes, :partial
31
32
 
32
33
  def initialize(paths, path, prefixes, partial, details, *)
34
+ if partial && path.present?
35
+ path = path.sub(%r{([^/]+)$}, "_\\1")
36
+ end
37
+
33
38
  @path = path
34
- prefixes = Array(prefixes)
39
+ @paths = paths
40
+ @prefixes = Array(prefixes)
41
+ @partial = partial
35
42
  template_type = if partial
36
43
  "partial"
37
44
  elsif /layouts/i.match?(path)
@@ -40,34 +47,135 @@ module ActionView
40
47
  "template"
41
48
  end
42
49
 
43
- if partial && path.present?
44
- path = path.sub(%r{([^/]+)$}, "_\\1")
45
- end
46
- searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
50
+ searched_paths = @prefixes.map { |prefix| [prefix, path].join("/") }
47
51
 
48
- out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
52
+ out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}.\n\nSearched in:\n"
49
53
  out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join
50
54
  super out
51
55
  end
56
+
57
+ if defined?(DidYouMean::Correctable) && defined?(DidYouMean::Jaro)
58
+ include DidYouMean::Correctable
59
+
60
+ class Results # :nodoc:
61
+ Result = Struct.new(:path, :score)
62
+
63
+ def initialize(size)
64
+ @size = size
65
+ @results = []
66
+ end
67
+
68
+ def to_a
69
+ @results.map(&:path)
70
+ end
71
+
72
+ def should_record?(score)
73
+ if @results.size < @size
74
+ true
75
+ else
76
+ score < @results.last.score
77
+ end
78
+ end
79
+
80
+ def add(path, score)
81
+ if should_record?(score)
82
+ @results << Result.new(path, score)
83
+ @results.sort_by!(&:score)
84
+ @results.pop if @results.size > @size
85
+ end
86
+ end
87
+ end
88
+
89
+ # Apps may have thousands of candidate templates so we attempt to
90
+ # generate the suggestions as efficiently as possible.
91
+ # First we split templates into prefixes and basenames, so that those can
92
+ # be matched separately.
93
+ def corrections
94
+ candidates = paths.flat_map(&:all_template_paths).uniq
95
+
96
+ if partial
97
+ candidates.select!(&:partial?)
98
+ else
99
+ candidates.reject!(&:partial?)
100
+ end
101
+
102
+ # Group by possible prefixes
103
+ files_by_dir = candidates.group_by(&:prefix)
104
+ files_by_dir.transform_values! do |files|
105
+ files.map do |file|
106
+ # Remove prefix
107
+ File.basename(file.to_s)
108
+ end
109
+ end
110
+
111
+ # No suggestions if there's an exact match, but wrong details
112
+ if prefixes.any? { |prefix| files_by_dir[prefix]&.include?(path) }
113
+ return []
114
+ end
115
+
116
+ cached_distance = Hash.new do |h, args|
117
+ h[args] = -DidYouMean::Jaro.distance(*args)
118
+ end
119
+
120
+ results = Results.new(6)
121
+
122
+ files_by_dir.keys.index_with do |dirname|
123
+ prefixes.map do |prefix|
124
+ cached_distance[[prefix, dirname]]
125
+ end.min
126
+ end.sort_by(&:last).each do |dirname, dirweight|
127
+ # If our directory's score makes it impossible to find a better match
128
+ # we can prune this search branch.
129
+ next unless results.should_record?(dirweight - 1.0)
130
+
131
+ files = files_by_dir[dirname]
132
+
133
+ files.each do |file|
134
+ fileweight = cached_distance[[path, file]]
135
+ score = dirweight + fileweight
136
+
137
+ results.add(File.join(dirname, file), score)
138
+ end
139
+ end
140
+
141
+ if partial
142
+ results.to_a.map { |res| res.sub(%r{_([^/]+)\z}, "\\1") }
143
+ else
144
+ results.to_a
145
+ end
146
+ end
147
+ end
52
148
  end
53
149
 
54
150
  class Template
55
151
  # The Template::Error exception is raised when the compilation or rendering of the template
56
152
  # fails. This exception then gathers a bunch of intimate details and uses it to report a
57
153
  # precise exception message.
58
- class Error < ActionViewError #:nodoc:
154
+ class Error < ActionViewError # :nodoc:
59
155
  SOURCE_CODE_RADIUS = 3
60
156
 
61
157
  # Override to prevent #cause resetting during re-raise.
62
158
  attr_reader :cause
63
159
 
160
+ attr_reader :template
161
+
64
162
  def initialize(template)
65
163
  super($!.message)
66
- set_backtrace($!.backtrace)
67
164
  @cause = $!
165
+ if @cause.is_a?(SyntaxError)
166
+ @cause = ActiveSupport::SyntaxErrorProxy.new(@cause)
167
+ end
68
168
  @template, @sub_templates = template, nil
69
169
  end
70
170
 
171
+ def backtrace
172
+ @cause.backtrace
173
+ end
174
+
175
+ def backtrace_locations
176
+ @cause.backtrace_locations
177
+ end
178
+
71
179
  def file_name
72
180
  @template.identifier
73
181
  end
@@ -134,7 +242,7 @@ module ActionView
134
242
 
135
243
  TemplateError = Template::Error
136
244
 
137
- class SyntaxErrorInTemplate < TemplateError #:nodoc
245
+ class SyntaxErrorInTemplate < TemplateError # :nodoc:
138
246
  def initialize(template, offending_code_string)
139
247
  @offending_code_string = offending_code_string
140
248
  super(template)
@@ -7,10 +7,10 @@ module ActionView
7
7
 
8
8
  def call(template, source)
9
9
  require_engine
10
- "xml = ::Builder::XmlMarkup.new(:indent => 2);" \
11
- "self.output_buffer = xml.target!;" +
12
- source +
13
- ";xml.target!;"
10
+ # the double assignment is to silence "assigned but unused variable" warnings
11
+ "xml = xml = ::Builder::XmlMarkup.new(indent: 2, target: output_buffer.raw);" \
12
+ "#{source};" \
13
+ "output_buffer.to_s"
14
14
  end
15
15
 
16
16
  private
@@ -16,22 +16,16 @@ module ActionView
16
16
 
17
17
  properties[:bufvar] ||= "@output_buffer"
18
18
  properties[:preamble] ||= ""
19
- properties[:postamble] ||= "#{properties[:bufvar]}.to_s"
19
+ properties[:postamble] ||= "#{properties[:bufvar]}"
20
+
21
+ # Tell Eruby that whether template will be compiled with `frozen_string_literal: true`
22
+ properties[:freeze_template_literals] = !Template.frozen_string_literal
20
23
 
21
24
  properties[:escapefunc] = ""
22
25
 
23
26
  super
24
27
  end
25
28
 
26
- def evaluate(action_view_erb_handler_context)
27
- src = @src
28
- view = Class.new(ActionView::Base) {
29
- include action_view_erb_handler_context._routes.url_helpers
30
- class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", defined?(@filename) ? @filename : "(erubi)", 0)
31
- }.empty
32
- view._run(:_template, nil, {}, ActionView::OutputBuffer.new)
33
- end
34
-
35
29
  private
36
30
  def add_text(text)
37
31
  return if text.empty?
@@ -39,30 +33,32 @@ module ActionView
39
33
  if text == "\n"
40
34
  @newline_pending += 1
41
35
  else
42
- src << bufvar << ".safe_append='"
43
- src << "\n" * @newline_pending if @newline_pending > 0
44
- src << text.gsub(/['\\]/, '\\\\\&')
45
- src << "'.freeze;"
46
-
36
+ with_buffer do
37
+ src << ".safe_append='"
38
+ src << "\n" * @newline_pending if @newline_pending > 0
39
+ src << text.gsub(/['\\]/, '\\\\\&') << @text_end
40
+ end
47
41
  @newline_pending = 0
48
42
  end
49
43
  end
50
44
 
51
- BLOCK_EXPR = /\s*((\s+|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
45
+ BLOCK_EXPR = /((\s|\))do|\{)(\s*\|[^|]*\|)?\s*\Z/
52
46
 
53
47
  def add_expression(indicator, code)
54
48
  flush_newline_if_pending(src)
55
49
 
56
- if (indicator == "==") || @escape
57
- src << bufvar << ".safe_expr_append="
58
- else
59
- src << bufvar << ".append="
60
- end
61
-
62
- if BLOCK_EXPR.match?(code)
63
- src << " " << code
64
- else
65
- src << "(" << code << ");"
50
+ with_buffer do
51
+ if (indicator == "==") || @escape
52
+ src << ".safe_expr_append="
53
+ else
54
+ src << ".append="
55
+ end
56
+
57
+ if BLOCK_EXPR.match?(code)
58
+ src << " " << code
59
+ else
60
+ src << "(" << code << ")"
61
+ end
66
62
  end
67
63
  end
68
64
 
@@ -78,7 +74,7 @@ module ActionView
78
74
 
79
75
  def flush_newline_if_pending(src)
80
76
  if @newline_pending > 0
81
- src << bufvar << ".safe_append='#{"\n" * @newline_pending}'.freeze;"
77
+ with_buffer { src << ".safe_append='#{"\n" * @newline_pending}" << @text_end }
82
78
  @newline_pending = 0
83
79
  end
84
80
  end