actionview 6.1.3.2 → 7.0.0.alpha2

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 +99 -262
  3. data/MIT-LICENSE +1 -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 +85 -30
  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 +5 -5
  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 +73 -30
  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 +17 -4
  33. data/lib/action_view/helpers/tags/base.rb +2 -14
  34. data/lib/action_view/helpers/tags/check_box.rb +1 -1
  35. data/lib/action_view/helpers/tags/collection_select.rb +1 -1
  36. data/lib/action_view/helpers/tags/time_field.rb +10 -1
  37. data/lib/action_view/helpers/tags/weekday_select.rb +27 -0
  38. data/lib/action_view/helpers/tags.rb +3 -2
  39. data/lib/action_view/helpers/text_helper.rb +24 -13
  40. data/lib/action_view/helpers/translation_helper.rb +4 -3
  41. data/lib/action_view/helpers/url_helper.rb +122 -80
  42. data/lib/action_view/helpers.rb +25 -25
  43. data/lib/action_view/lookup_context.rb +33 -52
  44. data/lib/action_view/model_naming.rb +1 -1
  45. data/lib/action_view/path_set.rb +16 -22
  46. data/lib/action_view/railtie.rb +15 -2
  47. data/lib/action_view/render_parser.rb +188 -0
  48. data/lib/action_view/renderer/abstract_renderer.rb +2 -2
  49. data/lib/action_view/renderer/partial_renderer.rb +0 -34
  50. data/lib/action_view/renderer/renderer.rb +4 -4
  51. data/lib/action_view/renderer/streaming_template_renderer.rb +3 -3
  52. data/lib/action_view/renderer/template_renderer.rb +6 -2
  53. data/lib/action_view/rendering.rb +2 -2
  54. data/lib/action_view/ripper_ast_parser.rb +198 -0
  55. data/lib/action_view/routing_url_for.rb +1 -1
  56. data/lib/action_view/template/error.rb +108 -13
  57. data/lib/action_view/template/handlers/erb.rb +6 -0
  58. data/lib/action_view/template/handlers.rb +3 -3
  59. data/lib/action_view/template/html.rb +3 -3
  60. data/lib/action_view/template/inline.rb +3 -3
  61. data/lib/action_view/template/raw_file.rb +3 -3
  62. data/lib/action_view/template/resolver.rb +84 -311
  63. data/lib/action_view/template/text.rb +3 -3
  64. data/lib/action_view/template/types.rb +14 -12
  65. data/lib/action_view/template.rb +10 -1
  66. data/lib/action_view/template_details.rb +66 -0
  67. data/lib/action_view/template_path.rb +64 -0
  68. data/lib/action_view/test_case.rb +6 -2
  69. data/lib/action_view/testing/resolvers.rb +11 -12
  70. data/lib/action_view/unbound_template.rb +33 -7
  71. data/lib/action_view.rb +3 -4
  72. data/lib/assets/compiled/rails-ujs.js +2 -2
  73. metadata +25 -18
@@ -16,6 +16,9 @@ module ActionView
16
16
  # Do not escape templates of these mime types.
17
17
  class_attribute :escape_ignore_list, default: ["text/plain"]
18
18
 
19
+ # Strip trailing newlines from rendered output
20
+ class_attribute :strip_trailing_newlines, default: false
21
+
19
22
  ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
20
23
 
21
24
  def self.call(template, source)
@@ -45,6 +48,9 @@ module ActionView
45
48
  # Always make sure we return a String in the default_internal
46
49
  erb.encode!
47
50
 
51
+ # Strip trailing newlines from the template if enabled
52
+ erb.chomp! if strip_trailing_newlines
53
+
48
54
  options = {
49
55
  escape: (self.class.escape_ignore_list.include? template.type),
50
56
  trim: (self.class.erb_trim_mode == "-")
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
3
+ module ActionView # :nodoc:
4
4
  # = Action View Template Handlers
5
- class Template #:nodoc:
6
- module Handlers #:nodoc:
5
+ class Template # :nodoc:
6
+ module Handlers # :nodoc:
7
7
  autoload :Raw, "action_view/template/handlers/raw"
8
8
  autoload :ERB, "action_view/template/handlers/erb"
9
9
  autoload :Html, "action_view/template/handlers/html"
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
3
+ module ActionView # :nodoc:
4
4
  # = Action View HTML Template
5
- class Template #:nodoc:
6
- class HTML #:nodoc:
5
+ class Template # :nodoc:
6
+ class HTML # :nodoc:
7
7
  attr_reader :type
8
8
 
9
9
  def initialize(string, type)
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
4
- class Template #:nodoc:
5
- class Inline < Template #:nodoc:
3
+ module ActionView # :nodoc:
4
+ class Template # :nodoc:
5
+ class Inline < Template # :nodoc:
6
6
  # This finalizer is needed (and exactly with a proc inside another proc)
7
7
  # otherwise templates leak in development.
8
8
  Finalizer = proc do |method_name, mod| # :nodoc:
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActionView #:nodoc:
3
+ module ActionView # :nodoc:
4
4
  # = Action View RawFile Template
5
- class Template #:nodoc:
6
- class RawFile #:nodoc:
5
+ class Template # :nodoc:
6
+ class RawFile # :nodoc:
7
7
  attr_accessor :type, :format
8
8
 
9
9
  def initialize(filename)
@@ -10,32 +10,12 @@ require "concurrent/map"
10
10
  module ActionView
11
11
  # = Action View Resolver
12
12
  class Resolver
13
- # Keeps all information about view path and builds virtual path.
14
- class Path
15
- attr_reader :name, :prefix, :partial, :virtual
16
- alias_method :partial?, :partial
17
-
18
- def self.build(name, prefix, partial)
19
- virtual = +""
20
- virtual << "#{prefix}/" unless prefix.empty?
21
- virtual << (partial ? "_#{name}" : name)
22
- new name, prefix, partial, virtual
23
- end
24
-
25
- def initialize(name, prefix, partial, virtual)
26
- @name = name
27
- @prefix = prefix
28
- @partial = partial
29
- @virtual = virtual
30
- end
31
-
32
- def to_str
33
- @virtual
34
- end
35
- alias :to_s :to_str
36
- end
13
+ Path = ActionView::TemplatePath
14
+ deprecate_constant :Path
37
15
 
38
16
  class PathParser # :nodoc:
17
+ ParsedPath = Struct.new(:path, :details)
18
+
39
19
  def build_path_regex
40
20
  handlers = Template::Handlers.extensions.map { |x| Regexp.escape(x) }.join("|")
41
21
  formats = Template::Types.symbols.map { |x| Regexp.escape(x) }.join("|")
@@ -58,79 +38,15 @@ module ActionView
58
38
  def parse(path)
59
39
  @regex ||= build_path_regex
60
40
  match = @regex.match(path)
61
- {
62
- prefix: match[:prefix] || "",
63
- action: match[:action],
64
- partial: !!match[:partial],
65
- locale: match[:locale]&.to_sym,
66
- handler: match[:handler]&.to_sym,
67
- format: match[:format]&.to_sym,
68
- variant: match[:variant]
69
- }
70
- end
71
- end
72
-
73
- # Threadsafe template cache
74
- class Cache #:nodoc:
75
- class SmallCache < Concurrent::Map
76
- def initialize(options = {})
77
- super(options.merge(initial_capacity: 2))
78
- end
79
- end
80
-
81
- # Preallocate all the default blocks for performance/memory consumption reasons
82
- PARTIAL_BLOCK = lambda { |cache, partial| cache[partial] = SmallCache.new }
83
- PREFIX_BLOCK = lambda { |cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK) }
84
- NAME_BLOCK = lambda { |cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK) }
85
- KEY_BLOCK = lambda { |cache, key| cache[key] = SmallCache.new(&NAME_BLOCK) }
86
-
87
- # Usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
88
- NO_TEMPLATES = [].freeze
89
-
90
- def initialize
91
- @data = SmallCache.new(&KEY_BLOCK)
92
- @query_cache = SmallCache.new
93
- end
94
-
95
- def inspect
96
- "#{to_s[0..-2]} keys=#{@data.size} queries=#{@query_cache.size}>"
97
- end
98
-
99
- # Cache the templates returned by the block
100
- def cache(key, name, prefix, partial, locals)
101
- @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
102
- end
103
-
104
- def cache_query(query) # :nodoc:
105
- @query_cache[query] ||= canonical_no_templates(yield)
106
- end
107
-
108
- def clear
109
- @data.clear
110
- @query_cache.clear
111
- end
112
-
113
- # Get the cache size. Do not call this
114
- # method. This method is not guaranteed to be here ever.
115
- def size # :nodoc:
116
- size = 0
117
- @data.each_value do |v1|
118
- v1.each_value do |v2|
119
- v2.each_value do |v3|
120
- v3.each_value do |v4|
121
- size += v4.size
122
- end
123
- end
124
- end
125
- end
126
-
127
- size + @query_cache.size
41
+ path = TemplatePath.build(match[:action], match[:prefix] || "", !!match[:partial])
42
+ details = TemplateDetails.new(
43
+ match[:locale]&.to_sym,
44
+ match[:handler]&.to_sym,
45
+ match[:format]&.to_sym,
46
+ match[:variant]&.to_sym
47
+ )
48
+ ParsedPath.new(path, details)
128
49
  end
129
-
130
- private
131
- def canonical_no_templates(templates)
132
- templates.empty? ? NO_TEMPLATES : templates
133
- end
134
50
  end
135
51
 
136
52
  cattr_accessor :caching, default: true
@@ -139,25 +55,17 @@ module ActionView
139
55
  alias :caching? :caching
140
56
  end
141
57
 
142
- def initialize
143
- @cache = Cache.new
144
- end
145
-
146
58
  def clear_cache
147
- @cache.clear
148
59
  end
149
60
 
150
61
  # Normalizes the arguments and passes it on to find_templates.
151
62
  def find_all(name, prefix = nil, partial = false, details = {}, key = nil, locals = [])
152
- locals = locals.map(&:to_s).sort!.freeze
153
-
154
- cached(key, [name, prefix, partial], details, locals) do
155
- _find_all(name, prefix, partial, details, key, locals)
156
- end
63
+ _find_all(name, prefix, partial, details, key, locals)
157
64
  end
158
65
 
159
- def find_all_with_query(query) # :nodoc:
160
- @cache.cache_query(query) { find_template_paths(File.join(@path, query)) }
66
+ def all_template_paths # :nodoc:
67
+ # Not implemented by default
68
+ []
161
69
  end
162
70
 
163
71
  private
@@ -173,34 +81,18 @@ module ActionView
173
81
  def find_templates(name, prefix, partial, details, locals = [])
174
82
  raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details, locals = []) method"
175
83
  end
176
-
177
- # Handles templates caching. If a key is given and caching is on
178
- # always check the cache before hitting the resolver. Otherwise,
179
- # it always hits the resolver but if the key is present, check if the
180
- # resolver is fresher before returning it.
181
- def cached(key, path_info, details, locals)
182
- name, prefix, partial = path_info
183
-
184
- if key
185
- @cache.cache(key, name, prefix, partial, locals) do
186
- yield
187
- end
188
- else
189
- yield
190
- end
191
- end
192
84
  end
193
85
 
194
- # An abstract class that implements a Resolver with path semantics.
195
- class PathResolver < Resolver #:nodoc:
196
- EXTENSIONS = { locale: ".", formats: ".", variants: "+", handlers: "." }
197
- DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}"
86
+ # A resolver that loads files from the filesystem.
87
+ class FileSystemResolver < Resolver
88
+ attr_reader :path
198
89
 
199
- def initialize
200
- @pattern = DEFAULT_PATTERN
90
+ def initialize(path)
91
+ raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
201
92
  @unbound_templates = Concurrent::Map.new
202
93
  @path_parser = PathParser.new
203
- super
94
+ @path = File.expand_path(path)
95
+ super()
204
96
  end
205
97
 
206
98
  def clear_cache
@@ -209,26 +101,37 @@ module ActionView
209
101
  super
210
102
  end
211
103
 
212
- private
213
- def _find_all(name, prefix, partial, details, key, locals)
214
- path = Path.build(name, prefix, partial)
215
- query(path, details, details[:formats], locals, cache: !!key)
104
+ def to_s
105
+ @path.to_s
106
+ end
107
+ alias :to_path :to_s
108
+
109
+ def eql?(resolver)
110
+ self.class.equal?(resolver.class) && to_path == resolver.to_path
111
+ end
112
+ alias :== :eql?
113
+
114
+ def all_template_paths # :nodoc:
115
+ paths = template_glob("**/*")
116
+ paths.map do |filename|
117
+ filename.from(@path.size + 1).remove(/\.[^\/]*\z/)
118
+ end.uniq.map do |filename|
119
+ TemplatePath.parse(filename)
216
120
  end
121
+ end
217
122
 
218
- def query(path, details, formats, locals, cache:)
219
- template_paths = find_template_paths_from_details(path, details)
220
- template_paths = reject_files_external_to_app(template_paths)
123
+ private
124
+ def _find_all(name, prefix, partial, details, key, locals)
125
+ requested_details = key || TemplateDetails::Requested.new(**details)
126
+ cache = key ? @unbound_templates : Concurrent::Map.new
221
127
 
222
- template_paths.map do |template|
223
- unbound_template =
224
- if cache
225
- @unbound_templates.compute_if_absent([template, path.virtual]) do
226
- build_unbound_template(template, path.virtual)
227
- end
228
- else
229
- build_unbound_template(template, path.virtual)
230
- end
128
+ unbound_templates =
129
+ cache.compute_if_absent(TemplatePath.virtual(name, prefix, partial)) do
130
+ path = TemplatePath.build(name, prefix, partial)
131
+ unbound_templates_from_path(path)
132
+ end
231
133
 
134
+ filter_and_sort_by_details(unbound_templates, requested_details).map do |unbound_template|
232
135
  unbound_template.bind_locals(locals)
233
136
  end
234
137
  end
@@ -237,196 +140,66 @@ module ActionView
237
140
  Template::Sources::File.new(template)
238
141
  end
239
142
 
240
- def build_unbound_template(template, virtual_path)
241
- handler, format, variant = extract_handler_and_format_and_variant(template)
143
+ def build_unbound_template(template)
144
+ parsed = @path_parser.parse(template.from(@path.size + 1))
145
+ details = parsed.details
242
146
  source = source_for_template(template)
243
147
 
244
148
  UnboundTemplate.new(
245
149
  source,
246
150
  template,
247
- handler,
248
- virtual_path: virtual_path,
249
- format: format,
250
- variant: variant,
151
+ details: details,
152
+ virtual_path: parsed.path.virtual,
251
153
  )
252
154
  end
253
155
 
254
- def reject_files_external_to_app(files)
255
- files.reject { |filename| !inside_path?(@path, filename) }
256
- end
257
-
258
- def find_template_paths_from_details(path, details)
156
+ def unbound_templates_from_path(path)
259
157
  if path.name.include?(".")
260
- ActiveSupport::Deprecation.warn("Rendering actions with '.' in the name is deprecated: #{path}")
261
- end
262
-
263
- query = build_query(path, details)
264
- find_template_paths(query)
265
- end
266
-
267
- def find_template_paths(query)
268
- Dir[query].uniq.reject do |filename|
269
- File.directory?(filename) ||
270
- # deals with case-insensitive file systems.
271
- !File.fnmatch(query, filename, File::FNM_EXTGLOB)
272
- end
273
- end
274
-
275
- def inside_path?(path, filename)
276
- filename = File.expand_path(filename)
277
- path = File.join(path, "")
278
- filename.start_with?(path)
279
- end
280
-
281
- # Helper for building query glob string based on resolver's pattern.
282
- def build_query(path, details)
283
- query = @pattern.dup
284
-
285
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
286
- query.gsub!(/:prefix(\/)?/, prefix)
287
-
288
- partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
289
- query.gsub!(":action", partial)
290
-
291
- details.each do |ext, candidates|
292
- if ext == :variants && candidates == :any
293
- query.gsub!(/:#{ext}/, "*")
294
- else
295
- query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
296
- end
158
+ return []
297
159
  end
298
160
 
299
- File.expand_path(query, @path)
300
- end
301
-
302
- def escape_entry(entry)
303
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
304
- end
305
-
306
- # Extract handler, formats and variant from path. If a format cannot be found neither
307
- # from the path, or the handler, we should return the array of formats given
308
- # to the resolver.
309
- def extract_handler_and_format_and_variant(path)
310
- details = @path_parser.parse(path)
311
-
312
- handler = Template.handler_for_extension(details[:handler])
313
- format = details[:format] || handler.try(:default_format)
314
- variant = details[:variant]
315
-
316
- # Template::Types[format] and handler.default_format can return nil
317
- [handler, format, variant]
318
- end
319
- end
320
-
321
- # A resolver that loads files from the filesystem.
322
- class FileSystemResolver < PathResolver
323
- attr_reader :path
324
-
325
- def initialize(path)
326
- raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
327
- super()
328
- @path = File.expand_path(path)
329
- end
330
-
331
- def to_s
332
- @path.to_s
333
- end
334
- alias :to_path :to_s
335
-
336
- def eql?(resolver)
337
- self.class.equal?(resolver.class) && to_path == resolver.to_path
338
- end
339
- alias :== :eql?
340
- end
341
-
342
- # An Optimized resolver for Rails' most common case.
343
- class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
344
- def initialize(path)
345
- super(path)
346
- end
347
-
348
- private
349
- def find_candidate_template_paths(path)
350
161
  # Instead of checking for every possible path, as our other globs would
351
162
  # do, scan the directory for files with the right prefix.
352
- query = "#{escape_entry(File.join(@path, path))}*"
163
+ paths = template_glob("#{escape_entry(path.to_s)}*")
353
164
 
354
- Dir[query].reject do |filename|
355
- File.directory?(filename)
165
+ paths.map do |path|
166
+ build_unbound_template(path)
167
+ end.select do |template|
168
+ # Select for exact virtual path match, including case sensitivity
169
+ template.virtual_path == path.virtual
356
170
  end
357
171
  end
358
172
 
359
- def find_template_paths_from_details(path, details)
360
- if path.name.include?(".")
361
- # Fall back to the unoptimized resolver, which will warn
362
- return super
173
+ def filter_and_sort_by_details(templates, requested_details)
174
+ filtered_templates = templates.select do |template|
175
+ template.details.matches?(requested_details)
363
176
  end
364
177
 
365
- candidates = find_candidate_template_paths(path)
366
-
367
- regex = build_regex(path, details)
368
-
369
- candidates.uniq.reject do |filename|
370
- # This regex match does double duty of finding only files which match
371
- # details (instead of just matching the prefix) and also filtering for
372
- # case-insensitive file systems.
373
- !regex.match?(filename) ||
374
- File.directory?(filename)
375
- end.sort_by do |filename|
376
- # Because we scanned the directory, instead of checking for files
377
- # one-by-one, they will be returned in an arbitrary order.
378
- # We can use the matches found by the regex and sort by their index in
379
- # details.
380
- match = filename.match(regex)
381
- EXTENSIONS.keys.map do |ext|
382
- if ext == :variants && details[ext] == :any
383
- match[ext].nil? ? 0 : 1
384
- elsif match[ext].nil?
385
- # No match should be last
386
- details[ext].length
387
- else
388
- found = match[ext].to_sym
389
- details[ext].index(found)
390
- end
178
+ if filtered_templates.count > 1
179
+ filtered_templates.sort_by! do |template|
180
+ template.details.sort_key_for(requested_details)
391
181
  end
392
182
  end
393
- end
394
183
 
395
- def build_regex(path, details)
396
- query = Regexp.escape(File.join(@path, path))
397
- exts = EXTENSIONS.map do |ext, prefix|
398
- match =
399
- if ext == :variants && details[ext] == :any
400
- ".*?"
401
- else
402
- arr = details[ext].compact
403
- arr.uniq!
404
- arr.map! { |e| Regexp.escape(e) }
405
- arr.join("|")
406
- end
407
- prefix = Regexp.escape(prefix)
408
- "(#{prefix}(?<#{ext}>#{match}))?"
409
- end.join
410
-
411
- %r{\A#{query}#{exts}\z}
184
+ filtered_templates
412
185
  end
413
- end
414
186
 
415
- # The same as FileSystemResolver but does not allow templates to store
416
- # a virtual path since it is invalid for such resolvers.
417
- class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
418
- private_class_method :new
187
+ # Safe glob within @path
188
+ def template_glob(glob)
189
+ query = File.join(escape_entry(@path), glob)
190
+ path_with_slash = File.join(@path, "")
419
191
 
420
- def self.instances
421
- [new(""), new("/")]
422
- end
192
+ Dir.glob(query).filter_map do |filename|
193
+ filename = File.expand_path(filename)
194
+ next if File.directory?(filename)
195
+ next unless filename.start_with?(path_with_slash)
423
196
 
424
- def build_unbound_template(template, _)
425
- super(template, nil)
426
- end
197
+ filename
198
+ end
199
+ end
427
200
 
428
- def reject_files_external_to_app(files)
429
- files
430
- end
201
+ def escape_entry(entry)
202
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
203
+ end
431
204
  end
432
205
  end