sprockets 3.7.3 → 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +2 -299
  3. data/README.md +21 -35
  4. data/bin/sprockets +11 -8
  5. data/lib/rake/sprocketstask.rb +2 -2
  6. data/lib/sprockets/asset.rb +8 -21
  7. data/lib/sprockets/autoload/babel.rb +7 -0
  8. data/lib/sprockets/autoload/jsminc.rb +7 -0
  9. data/lib/sprockets/autoload/sassc.rb +7 -0
  10. data/lib/sprockets/autoload.rb +3 -0
  11. data/lib/sprockets/babel_processor.rb +58 -0
  12. data/lib/sprockets/base.rb +8 -8
  13. data/lib/sprockets/bower.rb +4 -2
  14. data/lib/sprockets/bundle.rb +1 -1
  15. data/lib/sprockets/cache.rb +2 -4
  16. data/lib/sprockets/closure_compressor.rb +1 -2
  17. data/lib/sprockets/coffee_script_processor.rb +9 -3
  18. data/lib/sprockets/compressing.rb +2 -2
  19. data/lib/sprockets/configuration.rb +1 -7
  20. data/lib/sprockets/context.rb +10 -18
  21. data/lib/sprockets/digest_utils.rb +40 -52
  22. data/lib/sprockets/directive_processor.rb +10 -15
  23. data/lib/sprockets/erb_processor.rb +1 -13
  24. data/lib/sprockets/http_utils.rb +19 -4
  25. data/lib/sprockets/jsminc_compressor.rb +31 -0
  26. data/lib/sprockets/jst_processor.rb +10 -10
  27. data/lib/sprockets/loader.rb +34 -28
  28. data/lib/sprockets/manifest.rb +3 -35
  29. data/lib/sprockets/manifest_utils.rb +0 -2
  30. data/lib/sprockets/mime.rb +7 -62
  31. data/lib/sprockets/path_dependency_utils.rb +2 -11
  32. data/lib/sprockets/path_digest_utils.rb +1 -1
  33. data/lib/sprockets/path_utils.rb +43 -18
  34. data/lib/sprockets/preprocessors/default_source_map.rb +24 -0
  35. data/lib/sprockets/processing.rb +30 -61
  36. data/lib/sprockets/processor_utils.rb +27 -28
  37. data/lib/sprockets/resolve.rb +172 -92
  38. data/lib/sprockets/sass_cache_store.rb +1 -6
  39. data/lib/sprockets/sass_compressor.rb +14 -1
  40. data/lib/sprockets/sass_processor.rb +18 -8
  41. data/lib/sprockets/sassc_compressor.rb +30 -0
  42. data/lib/sprockets/sassc_processor.rb +68 -0
  43. data/lib/sprockets/server.rb +11 -22
  44. data/lib/sprockets/source_map_comment_processor.rb +29 -0
  45. data/lib/sprockets/source_map_processor.rb +40 -0
  46. data/lib/sprockets/source_map_utils.rb +345 -0
  47. data/lib/sprockets/transformers.rb +62 -35
  48. data/lib/sprockets/uglifier_compressor.rb +12 -5
  49. data/lib/sprockets/unloaded_asset.rb +12 -11
  50. data/lib/sprockets/uri_tar.rb +4 -2
  51. data/lib/sprockets/uri_utils.rb +5 -8
  52. data/lib/sprockets/utils.rb +30 -79
  53. data/lib/sprockets/version.rb +1 -1
  54. data/lib/sprockets.rb +80 -35
  55. metadata +70 -41
  56. data/LICENSE +0 -21
  57. data/lib/sprockets/coffee_script_template.rb +0 -17
  58. data/lib/sprockets/deprecation.rb +0 -90
  59. data/lib/sprockets/eco_template.rb +0 -17
  60. data/lib/sprockets/ejs_template.rb +0 -17
  61. data/lib/sprockets/engines.rb +0 -92
  62. data/lib/sprockets/erb_template.rb +0 -11
  63. data/lib/sprockets/legacy.rb +0 -330
  64. data/lib/sprockets/legacy_proc_processor.rb +0 -35
  65. data/lib/sprockets/legacy_tilt_processor.rb +0 -29
  66. data/lib/sprockets/sass_template.rb +0 -19
@@ -1,6 +1,5 @@
1
1
  require 'sprockets/asset'
2
2
  require 'sprockets/digest_utils'
3
- require 'sprockets/engines'
4
3
  require 'sprockets/errors'
5
4
  require 'sprockets/file_reader'
6
5
  require 'sprockets/mime'
@@ -18,7 +17,7 @@ module Sprockets
18
17
  # object.
19
18
  module Loader
20
19
  include DigestUtils, PathUtils, ProcessorUtils, URIUtils
21
- include Engines, Mime, Processing, Resolve, Transformers
20
+ include Mime, Processing, Resolve, Transformers
22
21
 
23
22
 
24
23
  # Public: Load Asset by Asset URI.
@@ -27,7 +26,6 @@ module Sprockets
27
26
  # and full path such as:
28
27
  # "file:///Path/app/assets/js/app.js?type=application/javascript"
29
28
  #
30
- #
31
29
  # Returns Asset.
32
30
  def load(uri)
33
31
  unloaded = UnloadedAsset.new(uri, self)
@@ -46,7 +44,7 @@ module Sprockets
46
44
  # The presence of `paths` indicates dependencies were stored.
47
45
  # We can check to see if the dependencies have not changed by "resolving" them and
48
46
  # generating a digest key from the resolved entries. If this digest key has not
49
- # changed the asset will be pulled from cache.
47
+ # changed, the asset will be pulled from cache.
50
48
  #
51
49
  # If this `paths` is present but the cache returns nothing then `fetch_asset_from_dependency_cache`
52
50
  # will confusingly be called again with `paths` set to nil where the asset will be
@@ -61,7 +59,7 @@ module Sprockets
61
59
  end
62
60
  end
63
61
  end
64
- Asset.new(self, asset)
62
+ Asset.new(asset)
65
63
  end
66
64
 
67
65
  private
@@ -103,13 +101,23 @@ module Sprockets
103
101
  raise FileNotFound, "could not find file: #{unloaded.filename}"
104
102
  end
105
103
 
106
- load_path, logical_path = paths_split(config[:paths], unloaded.filename)
104
+ path_to_split =
105
+ if index_alias = unloaded.params[:index_alias]
106
+ expand_from_root index_alias
107
+ else
108
+ unloaded.filename
109
+ end
110
+
111
+ load_path, logical_path = paths_split(config[:paths], path_to_split)
107
112
 
108
113
  unless load_path
109
- raise FileOutsidePaths, "#{unloaded.filename} is no longer under a load path: #{self.paths.join(', ')}"
114
+ target = path_to_split
115
+ target += " (index alias of #{unloaded.filename})" if unloaded.params[:index_alias]
116
+ raise FileOutsidePaths, "#{target} is no longer under a load path: #{self.paths.join(', ')}"
110
117
  end
111
118
 
112
- logical_path, file_type, engine_extnames, _ = parse_path_extnames(logical_path)
119
+ extname, file_type = match_path_extname(logical_path, mime_exts)
120
+ logical_path = logical_path.chomp(extname)
113
121
  name = logical_path
114
122
 
115
123
  if pipeline = unloaded.params[:pipeline]
@@ -124,22 +132,31 @@ module Sprockets
124
132
  raise ConversionError, "could not convert #{file_type.inspect} to #{type.inspect}"
125
133
  end
126
134
 
127
- processors = processors_for(type, file_type, engine_extnames, pipeline)
135
+ processors = processors_for(type, file_type, pipeline)
128
136
 
129
- processors_dep_uri = build_processors_uri(type, file_type, engine_extnames, pipeline)
137
+ processors_dep_uri = build_processors_uri(type, file_type, pipeline)
130
138
  dependencies = config[:dependencies] + [processors_dep_uri]
131
139
 
132
140
  # Read into memory and process if theres a processor pipeline
133
141
  if processors.any?
142
+ source_uri, _ = resolve!(unloaded.filename, pipeline: :source)
143
+ source_asset = load(source_uri)
144
+
145
+ source_path = source_asset.digest_path
146
+
134
147
  result = call_processors(processors, {
135
148
  environment: self,
136
149
  cache: self.cache,
137
150
  uri: unloaded.uri,
138
151
  filename: unloaded.filename,
139
152
  load_path: load_path,
153
+ source_path: source_path,
140
154
  name: name,
141
155
  content_type: type,
142
- metadata: { dependencies: dependencies }
156
+ metadata: {
157
+ dependencies: dependencies,
158
+ map: []
159
+ }
143
160
  })
144
161
  validate_processor_result!(result)
145
162
  source = result.delete(:data)
@@ -171,17 +188,6 @@ module Sprockets
171
188
  asset[:id] = pack_hexdigest(digest(asset))
172
189
  asset[:uri] = build_asset_uri(unloaded.filename, unloaded.params.merge(id: asset[:id]))
173
190
 
174
- # Deprecated: Avoid tracking Asset mtime
175
- asset[:mtime] = metadata[:dependencies].map { |u|
176
- if u.start_with?("file-digest:")
177
- s = self.stat(parse_file_digest_uri(u))
178
- s ? s.mtime.to_i : nil
179
- else
180
- nil
181
- end
182
- }.compact.max
183
- asset[:mtime] ||= self.stat(unloaded.filename).mtime.to_i
184
-
185
191
  store_asset(asset, unloaded)
186
192
  asset
187
193
  end
@@ -255,11 +261,11 @@ module Sprockets
255
261
  # "processors:type=text/css&file_type=text/css&pipeline=self",
256
262
  # "file-digest:///Full/path/app/assets/stylesheets"]
257
263
  #
258
- # Returns back array of things that the given uri dpends on
264
+ # Returns back array of things that the given uri depends on
259
265
  # For example the environment version, if you're using a different version of sprockets
260
266
  # then the dependencies should be different, this is used only for generating cache key
261
267
  # for example the "environment-version" may be resolved to "environment-1.0-3.2.0" for
262
- # version "3.2.0" of sprockets.
268
+ # version "3.2.0" of sprockets.
263
269
  #
264
270
  # Any paths that are returned are converted to relative paths
265
271
  #
@@ -276,9 +282,9 @@ module Sprockets
276
282
  #
277
283
  # This method attempts to retrieve the last `limit` number of histories of an asset
278
284
  # from the cache a "history" which is an array of unresolved "dependencies" that the asset needs
279
- # to compile. In this case A dependency can refer to either an asset i.e. index.js
280
- # may rely on jquery.js (so jquery.js is a depndency), or other factors that may affect
281
- # compilation, such as the VERSION of sprockets (i.e. the environment) and what "processors"
285
+ # to compile. In this case a dependency can refer to either an asset e.g. index.js
286
+ # may rely on jquery.js (so jquery.js is a dependency), or other factors that may affect
287
+ # compilation, such as the VERSION of Sprockets (i.e. the environment) and what "processors"
282
288
  # are used.
283
289
  #
284
290
  # For example a history array may look something like this
@@ -289,7 +295,7 @@ module Sprockets
289
295
  # "file-digest:///Full/path/app/assets/stylesheets"]]
290
296
  #
291
297
  # Where the first entry is a Set of dependencies for last generated version of that asset.
292
- # Multiple versions are stored since sprockets keeps the last `limit` number of assets
298
+ # Multiple versions are stored since Sprockets keeps the last `limit` number of assets
293
299
  # generated present in the system.
294
300
  #
295
301
  # If a "history" of dependencies is present in the cache, each version of "history" will be
@@ -52,14 +52,8 @@ module Sprockets
52
52
  @directory ||= File.dirname(@filename) if @filename
53
53
 
54
54
  # If directory is given w/o filename, pick a random manifest location
55
- @rename_filename = nil
56
55
  if @directory && @filename.nil?
57
56
  @filename = find_directory_manifest(@directory)
58
-
59
- # If legacy manifest name autodetected, mark to rename on save
60
- if File.basename(@filename).start_with?("manifest")
61
- @rename_filename = File.join(@directory, generate_manifest_path)
62
- end
63
57
  end
64
58
 
65
59
  unless @directory && @filename
@@ -125,27 +119,13 @@ module Sprockets
125
119
 
126
120
  return to_enum(__method__, *args) unless block_given?
127
121
 
128
- paths, filters = args.flatten.partition { |arg| self.class.simple_logical_path?(arg) }
129
- filters = filters.map { |arg| self.class.compile_match_filter(arg) }
130
-
131
122
  environment = self.environment.cached
132
-
133
- paths.each do |path|
123
+ args.flatten.each do |path|
134
124
  environment.find_all_linked_assets(path) do |asset|
135
125
  yield asset
136
126
  end
137
127
  end
138
128
 
139
- if filters.any?
140
- environment.logical_paths do |logical_path, filename|
141
- if filters.any? { |f| f.call(logical_path, filename) }
142
- environment.find_all_linked_assets(filename) do |asset|
143
- yield asset
144
- end
145
- end
146
- end
147
- end
148
-
149
129
  nil
150
130
  end
151
131
 
@@ -161,8 +141,7 @@ module Sprockets
161
141
  end
162
142
  else
163
143
  args.each do |path|
164
- asset = assets[path]
165
- yield File.binread(File.join(dir, asset)) if asset
144
+ yield File.binread(File.join(dir, assets[path]))
166
145
  end
167
146
  end
168
147
  end
@@ -186,7 +165,7 @@ module Sprockets
186
165
  find(*args) do |asset|
187
166
  files[asset.digest_path] = {
188
167
  'logical_path' => asset.logical_path,
189
- 'mtime' => asset.mtime.iso8601,
168
+ 'mtime' => Time.now.iso8601,
190
169
  'size' => asset.bytesize,
191
170
  'digest' => asset.hexdigest,
192
171
 
@@ -197,10 +176,6 @@ module Sprockets
197
176
  }
198
177
  assets[asset.logical_path] = asset.digest_path
199
178
 
200
- if alias_logical_path = self.class.compute_alias_logical_path(asset.logical_path)
201
- assets[alias_logical_path] = asset.digest_path
202
- end
203
-
204
179
  target = File.join(dir, asset.digest_path)
205
180
 
206
181
  if File.exist?(target)
@@ -300,13 +275,6 @@ module Sprockets
300
275
 
301
276
  # Persist manfiest back to FS
302
277
  def save
303
- if @rename_filename
304
- logger.info "Renaming #{@filename} to #{@rename_filename}"
305
- FileUtils.mv(@filename, @rename_filename)
306
- @filename = @rename_filename
307
- @rename_filename = nil
308
- end
309
-
310
278
  data = json_encode(@data)
311
279
  FileUtils.mkdir_p File.dirname(@filename)
312
280
  PathUtils.atomic_write(@filename) do |f|
@@ -36,8 +36,6 @@ module Sprockets
36
36
  def find_directory_manifest(dirname)
37
37
  entries = File.directory?(dirname) ? Dir.entries(dirname) : []
38
38
  entry = entries.find { |e| e =~ MANIFEST_RE } ||
39
- # Deprecated: Will be removed in 4.x
40
- entries.find { |e| e =~ LEGACY_MANIFEST_RE } ||
41
39
  generate_manifest_path
42
40
  File.join(dirname, entry)
43
41
  end
@@ -36,29 +36,18 @@ module Sprockets
36
36
 
37
37
  # Public: Register a new mime type.
38
38
  #
39
- # mime_type - String MIME Type
40
- # options - Hash
41
- # extensions: Array of String extnames
42
- # charset: Proc/Method that detects the charset of a file.
43
- # See EncodingUtils.
39
+ # mime_type - String MIME Type
40
+ # extensions - Array of String extnames
41
+ # charset - Proc/Method that detects the charset of a file.
42
+ # See EncodingUtils.
44
43
  #
45
44
  # Returns nothing.
46
- def register_mime_type(mime_type, options = {})
47
- # Legacy extension argument, will be removed from 4.x
48
- if options.is_a?(String)
49
- options = { extensions: [options] }
50
- end
51
-
52
- extnames = Array(options[:extensions]).map { |extname|
53
- Sprockets::Utils.normalize_extension(extname)
54
- }
45
+ def register_mime_type(mime_type, extensions: [], charset: nil)
46
+ extnames = Array(extensions)
55
47
 
56
- charset = options[:charset]
57
48
  charset ||= :default if mime_type.start_with?('text/')
58
49
  charset = EncodingUtils::CHARSET_DETECT[charset] if charset.is_a?(Symbol)
59
50
 
60
- self.computed_config = {}
61
-
62
51
  self.config = hash_reassoc(config, :mime_exts) do |mime_exts|
63
52
  extnames.each do |extname|
64
53
  mime_exts[extname] = mime_type
@@ -97,54 +86,10 @@ module Sprockets
97
86
  data = File.binread(filename)
98
87
 
99
88
  if detect = mime_type_charset_detecter(content_type)
100
- detect.call(data).encode(Encoding::UTF_8, :universal_newline => true)
89
+ detect.call(data).encode(Encoding::UTF_8, universal_newline: true)
101
90
  else
102
91
  data
103
92
  end
104
93
  end
105
-
106
- private
107
- def extname_map
108
- self.computed_config[:_extnames] ||= compute_extname_map
109
- end
110
-
111
- def compute_extname_map
112
- graph = {}
113
-
114
- engine_extname_permutation = []
115
-
116
- 4.times do |n|
117
- config[:engines].keys.permutation(n).each do |engine_extnames|
118
- engine_extname_permutation << engine_extnames
119
- end
120
- end
121
-
122
- mime_exts_grouped_by_mime_type = {}
123
- config[:mime_exts].each do |format_extname,format_type|
124
- mime_exts_grouped_by_mime_type[format_type] ||= []
125
- mime_exts_grouped_by_mime_type[format_type] << format_extname
126
- end
127
-
128
- ([nil] + pipelines.keys.map(&:to_s)).each do |pipeline|
129
- pipeline_extname = pipeline ? ".#{pipeline}" : ''.freeze
130
- engine_extname_permutation.each do |engine_extnames|
131
- mime_exts_grouped_by_mime_type.each do |format_type, format_extnames|
132
- type = format_type
133
- value = [type, engine_extnames, pipeline]
134
- format_extnames.each do |format_extname|
135
- key = "#{pipeline_extname}#{format_extname}#{engine_extnames.join}"
136
- graph[key] = value
137
- end
138
- if format_type == config[:engine_mime_types][engine_extnames.first]
139
- key = "#{pipeline_extname}#{engine_extnames.join}"
140
- graph[key] = value
141
- end
142
- end
143
- end
144
- graph[pipeline_extname] = [nil, [], pipeline]
145
- end
146
-
147
- graph
148
- end
149
94
  end
150
95
  end
@@ -41,7 +41,7 @@ module Sprockets
41
41
  #
42
42
  # Returns an Array of entry names and a Set of dependency URIs.
43
43
  def entries_with_dependencies(path)
44
- return entries(path), file_digest_dependency_set(path)
44
+ return entries(path), Set.new([build_file_digest_uri(path)])
45
45
  end
46
46
 
47
47
  # Internal: List directory filenames and associated Stats under a
@@ -53,16 +53,7 @@ module Sprockets
53
53
  #
54
54
  # Returns an Array of filenames and a Set of dependency URIs.
55
55
  def stat_directory_with_dependencies(dir)
56
- return stat_directory(dir).to_a, file_digest_dependency_set(dir)
57
- end
58
-
59
- # Internal: Returns a set of dependencies for a particular path.
60
- #
61
- # path - String directory path
62
- #
63
- # Returns a Set of dependency URIs.
64
- def file_digest_dependency_set(path)
65
- Set.new([build_file_digest_uri(path)])
56
+ return stat_directory(dir).to_a, Set.new([build_file_digest_uri(dir)])
66
57
  end
67
58
 
68
59
  # Internal: List directory filenames and associated Stats under an entire
@@ -15,7 +15,7 @@ module Sprockets
15
15
  def stat_digest(path, stat)
16
16
  if stat.directory?
17
17
  # If its a directive, digest the list of filenames
18
- digest_class.digest(self.entries(path).join(','))
18
+ digest_class.digest(self.entries(path).join(','.freeze))
19
19
  elsif stat.file?
20
20
  # If its a file, digest the contents
21
21
  digest_class.file(path.to_s).digest
@@ -1,3 +1,5 @@
1
+ require 'fileutils'
2
+
1
3
  module Sprockets
2
4
  # Internal: File and path related utilities. Mixed into Environment.
3
5
  #
@@ -53,13 +55,12 @@ module Sprockets
53
55
  # Returns an empty `Array` if the directory does not exist.
54
56
  def entries(path)
55
57
  if File.directory?(path)
56
- entries = Dir.entries(path, :encoding => Encoding.default_internal)
58
+ entries = Dir.entries(path, encoding: Encoding.default_internal)
57
59
  entries.reject! { |entry|
58
- entry.start_with?(".".freeze) ||
59
- (entry.start_with?("#".freeze) && entry.end_with?("#".freeze)) ||
60
- entry.end_with?("~".freeze)
60
+ entry =~ /^\.|~$|^\#.*\#$/
61
61
  }
62
62
  entries.sort!
63
+ entries
63
64
  else
64
65
  []
65
66
  end
@@ -109,7 +110,7 @@ module Sprockets
109
110
  # subpath is outside of path.
110
111
  def split_subpath(path, subpath)
111
112
  return "" if path == subpath
112
- path = File.join(path, '')
113
+ path = File.join(path, ''.freeze)
113
114
  if subpath.start_with?(path)
114
115
  subpath[path.length..-1]
115
116
  else
@@ -148,19 +149,43 @@ module Sprockets
148
149
  #
149
150
  # Returns [String extname, Object value] or nil nothing matched.
150
151
  def match_path_extname(path, extensions)
151
- basename = File.basename(path)
152
-
153
- i = basename.index('.'.freeze)
154
- while i && i < basename.length - 1
155
- extname = basename[i..-1]
156
- if value = extensions[extname]
157
- return extname, value
152
+ match, key = nil, ""
153
+ path_extnames(path).reverse_each do |extname|
154
+ key.prepend(extname)
155
+ if value = extensions[key]
156
+ match = [key.dup, value]
157
+ elsif match
158
+ break
158
159
  end
159
-
160
- i = basename.index('.'.freeze, i+1)
161
160
  end
161
+ match
162
+ end
162
163
 
163
- nil
164
+ # Internal: Match paths in a directory against available extensions.
165
+ #
166
+ # path - String directory
167
+ # basename - String basename of target file
168
+ # extensions - Hash of String extnames to values
169
+ #
170
+ # Examples
171
+ #
172
+ # exts = { ".js" => "application/javascript" }
173
+ # find_matching_path_for_extensions("app/assets", "application", exts)
174
+ # # => ["app/assets/application.js", "application/javascript"]
175
+ #
176
+ # Returns an Array of [String path, Object value] matches.
177
+ def find_matching_path_for_extensions(path, basename, extensions)
178
+ matches = []
179
+ entries(path).each do |entry|
180
+ extname, value = match_path_extname(entry, extensions)
181
+ if basename == entry.chomp(extname)
182
+ filename = File.join(path, entry)
183
+ if file?(filename)
184
+ matches << [filename, value]
185
+ end
186
+ end
187
+ end
188
+ matches
164
189
  end
165
190
 
166
191
  # Internal: Returns all parents for path
@@ -272,16 +297,16 @@ module Sprockets
272
297
  Thread.current.object_id,
273
298
  Process.pid,
274
299
  rand(1000000)
275
- ].join('.')
300
+ ].join('.'.freeze)
276
301
  tmpname = File.join(dirname, basename)
277
302
 
278
303
  File.open(tmpname, 'wb+') do |f|
279
304
  yield f
280
305
  end
281
306
 
282
- File.rename(tmpname, filename)
307
+ FileUtils.mv(tmpname, filename)
283
308
  ensure
284
- File.delete(tmpname) if File.exist?(tmpname)
309
+ FileUtils.rm(tmpname) if File.exist?(tmpname)
285
310
  end
286
311
  end
287
312
  end
@@ -0,0 +1,24 @@
1
+ module Sprockets
2
+ module Preprocessors
3
+ # Private: Adds a default map to assets when one is not present
4
+ #
5
+ # If the input file already has a source map, it effectively returns the original
6
+ # result. Otherwise it maps 1 for 1 lines original to generated. This is needed
7
+ # Because other generators run after might depend on having a valid source map
8
+ # available.
9
+ class DefaultSourceMap
10
+ def call(input)
11
+ result = { data: input[:data] }
12
+ map = input[:metadata][:map]
13
+ if map.nil? || map.empty?
14
+ result[:map] ||= []
15
+ input[:data].each_line.with_index do |_, index|
16
+ line = index + 1
17
+ result[:map] << { source: input[:source_path], generated: [line , 0], original: [line, 0] }
18
+ end
19
+ end
20
+ return result
21
+ end
22
+ end
23
+ end
24
+ end