sprockets 2.12.5 → 3.7.2

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +296 -0
  3. data/LICENSE +2 -2
  4. data/README.md +235 -262
  5. data/bin/sprockets +1 -0
  6. data/lib/rake/sprocketstask.rb +5 -4
  7. data/lib/sprockets/asset.rb +143 -212
  8. data/lib/sprockets/autoload/closure.rb +7 -0
  9. data/lib/sprockets/autoload/coffee_script.rb +7 -0
  10. data/lib/sprockets/autoload/eco.rb +7 -0
  11. data/lib/sprockets/autoload/ejs.rb +7 -0
  12. data/lib/sprockets/autoload/sass.rb +7 -0
  13. data/lib/sprockets/autoload/uglifier.rb +7 -0
  14. data/lib/sprockets/autoload/yui.rb +7 -0
  15. data/lib/sprockets/autoload.rb +11 -0
  16. data/lib/sprockets/base.rb +56 -393
  17. data/lib/sprockets/bower.rb +58 -0
  18. data/lib/sprockets/bundle.rb +69 -0
  19. data/lib/sprockets/cache/file_store.rb +168 -14
  20. data/lib/sprockets/cache/memory_store.rb +66 -0
  21. data/lib/sprockets/cache/null_store.rb +46 -0
  22. data/lib/sprockets/cache.rb +236 -0
  23. data/lib/sprockets/cached_environment.rb +69 -0
  24. data/lib/sprockets/closure_compressor.rb +35 -10
  25. data/lib/sprockets/coffee_script_processor.rb +25 -0
  26. data/lib/sprockets/coffee_script_template.rb +17 -0
  27. data/lib/sprockets/compressing.rb +44 -23
  28. data/lib/sprockets/configuration.rb +83 -0
  29. data/lib/sprockets/context.rb +86 -144
  30. data/lib/sprockets/dependencies.rb +73 -0
  31. data/lib/sprockets/deprecation.rb +90 -0
  32. data/lib/sprockets/digest_utils.rb +180 -0
  33. data/lib/sprockets/directive_processor.rb +207 -211
  34. data/lib/sprockets/eco_processor.rb +32 -0
  35. data/lib/sprockets/eco_template.rb +9 -30
  36. data/lib/sprockets/ejs_processor.rb +31 -0
  37. data/lib/sprockets/ejs_template.rb +9 -29
  38. data/lib/sprockets/encoding_utils.rb +261 -0
  39. data/lib/sprockets/engines.rb +53 -35
  40. data/lib/sprockets/environment.rb +17 -64
  41. data/lib/sprockets/erb_processor.rb +30 -0
  42. data/lib/sprockets/erb_template.rb +11 -0
  43. data/lib/sprockets/errors.rb +4 -13
  44. data/lib/sprockets/file_reader.rb +15 -0
  45. data/lib/sprockets/http_utils.rb +117 -0
  46. data/lib/sprockets/jst_processor.rb +35 -15
  47. data/lib/sprockets/legacy.rb +330 -0
  48. data/lib/sprockets/legacy_proc_processor.rb +35 -0
  49. data/lib/sprockets/legacy_tilt_processor.rb +29 -0
  50. data/lib/sprockets/loader.rb +325 -0
  51. data/lib/sprockets/manifest.rb +202 -127
  52. data/lib/sprockets/manifest_utils.rb +45 -0
  53. data/lib/sprockets/mime.rb +112 -31
  54. data/lib/sprockets/path_dependency_utils.rb +85 -0
  55. data/lib/sprockets/path_digest_utils.rb +47 -0
  56. data/lib/sprockets/path_utils.rb +287 -0
  57. data/lib/sprockets/paths.rb +42 -19
  58. data/lib/sprockets/processing.rb +178 -126
  59. data/lib/sprockets/processor_utils.rb +180 -0
  60. data/lib/sprockets/resolve.rb +211 -0
  61. data/lib/sprockets/sass_cache_store.rb +22 -17
  62. data/lib/sprockets/sass_compressor.rb +39 -15
  63. data/lib/sprockets/sass_functions.rb +2 -70
  64. data/lib/sprockets/sass_importer.rb +2 -30
  65. data/lib/sprockets/sass_processor.rb +292 -0
  66. data/lib/sprockets/sass_template.rb +12 -59
  67. data/lib/sprockets/server.rb +129 -84
  68. data/lib/sprockets/transformers.rb +145 -0
  69. data/lib/sprockets/uglifier_compressor.rb +39 -12
  70. data/lib/sprockets/unloaded_asset.rb +137 -0
  71. data/lib/sprockets/uri_tar.rb +98 -0
  72. data/lib/sprockets/uri_utils.rb +188 -0
  73. data/lib/sprockets/utils/gzip.rb +67 -0
  74. data/lib/sprockets/utils.rb +210 -44
  75. data/lib/sprockets/version.rb +1 -1
  76. data/lib/sprockets/yui_compressor.rb +39 -11
  77. data/lib/sprockets.rb +142 -81
  78. metadata +96 -90
  79. data/lib/sprockets/asset_attributes.rb +0 -137
  80. data/lib/sprockets/bundled_asset.rb +0 -78
  81. data/lib/sprockets/caching.rb +0 -96
  82. data/lib/sprockets/charset_normalizer.rb +0 -41
  83. data/lib/sprockets/index.rb +0 -100
  84. data/lib/sprockets/processed_asset.rb +0 -152
  85. data/lib/sprockets/processor.rb +0 -32
  86. data/lib/sprockets/safety_colons.rb +0 -28
  87. data/lib/sprockets/scss_template.rb +0 -13
  88. data/lib/sprockets/static_asset.rb +0 -60
@@ -1,26 +1,31 @@
1
- require 'multi_json'
2
- require 'securerandom'
1
+ require 'json'
3
2
  require 'time'
4
3
 
4
+ require 'concurrent'
5
+
6
+ require 'sprockets/manifest_utils'
7
+ require 'sprockets/utils/gzip'
8
+
5
9
  module Sprockets
6
- # The Manifest logs the contents of assets compiled to a single
7
- # directory. It records basic attributes about the asset for fast
8
- # lookup without having to compile. A pointer from each logical path
9
- # indicates with fingerprinted asset is the current one.
10
+ # The Manifest logs the contents of assets compiled to a single directory. It
11
+ # records basic attributes about the asset for fast lookup without having to
12
+ # compile. A pointer from each logical path indicates which fingerprinted
13
+ # asset is the current one.
10
14
  #
11
- # The JSON is part of the public API and should be considered
12
- # stable. This should make it easy to read from other programming
13
- # languages and processes that don't have sprockets loaded. See
14
- # `#assets` and `#files` for more infomation about the structure.
15
+ # The JSON is part of the public API and should be considered stable. This
16
+ # should make it easy to read from other programming languages and processes
17
+ # that don't have sprockets loaded. See `#assets` and `#files` for more
18
+ # infomation about the structure.
15
19
  class Manifest
16
- attr_reader :environment, :path, :dir
17
-
18
- # Create new Manifest associated with an `environment`. `path` is
19
- # a full path to the manifest json file. The file may or may not
20
- # already exist. The dirname of the `path` will be used to write
21
- # compiled assets to. Otherwise, if the path is a directory, the
22
- # filename will default a random "manifest-123.json" file in that
23
- # directory.
20
+ include ManifestUtils
21
+
22
+ attr_reader :environment
23
+
24
+ # Create new Manifest associated with an `environment`. `filename` is a full
25
+ # path to the manifest json file. The file may or may not already exist. The
26
+ # dirname of the `filename` will be used to write compiled assets to.
27
+ # Otherwise, if the path is a directory, the filename will default a random
28
+ # ".sprockets-manifest-*.json" file in that directory.
24
29
  #
25
30
  # Manifest.new(environment, "./public/assets/manifest.json")
26
31
  #
@@ -29,48 +34,58 @@ module Sprockets
29
34
  @environment = args.shift
30
35
  end
31
36
 
32
- @dir, @path = args[0], args[1]
37
+ @directory, @filename = args[0], args[1]
38
+
39
+ # Whether the manifest file is using the old manifest-*.json naming convention
40
+ @legacy_manifest = false
33
41
 
34
42
  # Expand paths
35
- @dir = File.expand_path(@dir) if @dir
36
- @path = File.expand_path(@path) if @path
43
+ @directory = File.expand_path(@directory) if @directory
44
+ @filename = File.expand_path(@filename) if @filename
37
45
 
38
- # If path is given as the second arg
39
- if @dir && File.extname(@dir) != ""
40
- @dir, @path = nil, @dir
46
+ # If filename is given as the second arg
47
+ if @directory && File.extname(@directory) != ""
48
+ @directory, @filename = nil, @directory
41
49
  end
42
50
 
43
- # Default dir to the directory of the path
44
- @dir ||= File.dirname(@path) if @path
51
+ # Default dir to the directory of the filename
52
+ @directory ||= File.dirname(@filename) if @filename
45
53
 
46
- # If directory is given w/o path, pick a random manifest.json location
47
- if @dir && @path.nil?
48
- # Find the first manifest.json in the directory
49
- paths = Dir[File.join(@dir, "manifest*.json")]
50
- if paths.any?
51
- @path = paths.first
52
- else
53
- @path = File.join(@dir, "manifest-#{SecureRandom.hex(16)}.json")
54
+ # If directory is given w/o filename, pick a random manifest location
55
+ @rename_filename = nil
56
+ if @directory && @filename.nil?
57
+ @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)
54
62
  end
55
63
  end
56
64
 
57
- unless @dir && @path
58
- raise ArgumentError, "manifest requires output path"
65
+ unless @directory && @filename
66
+ raise ArgumentError, "manifest requires output filename"
59
67
  end
60
68
 
61
- data = nil
69
+ data = {}
62
70
 
63
71
  begin
64
- if File.exist?(@path)
65
- data = json_decode(File.read(@path))
72
+ if File.exist?(@filename)
73
+ data = json_decode(File.read(@filename))
66
74
  end
67
- rescue MultiJson::DecodeError => e
68
- logger.error "#{@path} is invalid: #{e.class} #{e.message}"
75
+ rescue JSON::ParserError => e
76
+ logger.error "#{@filename} is invalid: #{e.class} #{e.message}"
69
77
  end
70
78
 
71
- @data = data.is_a?(Hash) ? data : {}
79
+ @data = data
72
80
  end
73
81
 
82
+ # Returns String path to manifest.json file.
83
+ attr_reader :filename
84
+ alias_method :path, :filename
85
+
86
+ attr_reader :directory
87
+ alias_method :dir, :directory
88
+
74
89
  # Returns internal assets mapping. Keys are logical paths which
75
90
  # map to the latest fingerprinted filename.
76
91
  #
@@ -100,6 +115,58 @@ module Sprockets
100
115
  @data['files'] ||= {}
101
116
  end
102
117
 
118
+ # Public: Find all assets matching pattern set in environment.
119
+ #
120
+ # Returns Enumerator of Assets.
121
+ def find(*args)
122
+ unless environment
123
+ raise Error, "manifest requires environment for compilation"
124
+ end
125
+
126
+ return to_enum(__method__, *args) unless block_given?
127
+
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
+ environment = self.environment.cached
132
+
133
+ paths.each do |path|
134
+ environment.find_all_linked_assets(path) do |asset|
135
+ yield asset
136
+ end
137
+ end
138
+
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
+ nil
150
+ end
151
+
152
+ # Public: Find the source of assets by paths.
153
+ #
154
+ # Returns Enumerator of assets file content.
155
+ def find_sources(*args)
156
+ return to_enum(__method__, *args) unless block_given?
157
+
158
+ if environment
159
+ find(*args).each do |asset|
160
+ yield asset.source
161
+ end
162
+ else
163
+ args.each do |path|
164
+ asset = assets[path]
165
+ yield File.binread(File.join(dir, asset)) if asset
166
+ end
167
+ end
168
+ end
169
+
103
170
  # Compile and write asset to directory. The asset is written to a
104
171
  # fingerprinted filename like
105
172
  # `application-2e8e9a7c6b0aafa0c9bdeec90ea30213.js`. An entry is
@@ -112,33 +179,59 @@ module Sprockets
112
179
  raise Error, "manifest requires environment for compilation"
113
180
  end
114
181
 
115
- paths = environment.each_logical_path(*args).to_a +
116
- args.flatten.select { |fn| Pathname.new(fn).absolute? if fn.is_a?(String)}
182
+ filenames = []
183
+ concurrent_compressors = []
184
+ concurrent_writers = []
185
+
186
+ find(*args) do |asset|
187
+ files[asset.digest_path] = {
188
+ 'logical_path' => asset.logical_path,
189
+ 'mtime' => asset.mtime.iso8601,
190
+ 'size' => asset.bytesize,
191
+ 'digest' => asset.hexdigest,
192
+
193
+ # Deprecated: Remove beta integrity attribute in next release.
194
+ # Callers should DigestUtils.hexdigest_integrity_uri to compute the
195
+ # digest themselves.
196
+ 'integrity' => DigestUtils.hexdigest_integrity_uri(asset.hexdigest)
197
+ }
198
+ assets[asset.logical_path] = asset.digest_path
199
+
200
+ if alias_logical_path = self.class.compute_alias_logical_path(asset.logical_path)
201
+ assets[alias_logical_path] = asset.digest_path
202
+ end
117
203
 
118
- paths.each do |path|
119
- if asset = find_asset(path)
120
- files[asset.digest_path] = {
121
- 'logical_path' => asset.logical_path,
122
- 'mtime' => asset.mtime.iso8601,
123
- 'size' => asset.bytesize,
124
- 'digest' => asset.digest
125
- }
126
- assets[asset.logical_path] = asset.digest_path
127
-
128
- target = File.join(dir, asset.digest_path)
129
-
130
- if File.exist?(target)
131
- logger.debug "Skipping #{target}, already exists"
132
- else
133
- logger.info "Writing #{target}"
134
- asset.write_to target
135
- asset.write_to "#{target}.gz" if asset.is_a?(BundledAsset)
136
- end
204
+ target = File.join(dir, asset.digest_path)
137
205
 
206
+ if File.exist?(target)
207
+ logger.debug "Skipping #{target}, already exists"
208
+ else
209
+ logger.info "Writing #{target}"
210
+ write_file = Concurrent::Future.execute { asset.write_to target }
211
+ concurrent_writers << write_file
138
212
  end
213
+ filenames << asset.filename
214
+
215
+ next if environment.skip_gzip?
216
+ gzip = Utils::Gzip.new(asset)
217
+ next if gzip.cannot_compress?(environment.mime_types)
218
+
219
+ if File.exist?("#{target}.gz")
220
+ logger.debug "Skipping #{target}.gz, already exists"
221
+ else
222
+ logger.info "Writing #{target}.gz"
223
+ concurrent_compressors << Concurrent::Future.execute do
224
+ write_file.wait! if write_file
225
+ gzip.compress(target)
226
+ end
227
+ end
228
+
139
229
  end
230
+ concurrent_writers.each(&:wait!)
231
+ concurrent_compressors.each(&:wait!)
140
232
  save
141
- paths
233
+
234
+ filenames
142
235
  end
143
236
 
144
237
  # Removes file from directory and from manifest. `filename` must
@@ -167,79 +260,67 @@ module Sprockets
167
260
  end
168
261
 
169
262
  # Cleanup old assets in the compile directory. By default it will
170
- # keep the latest version plus 2 backups.
171
- def clean(keep = 2)
172
- self.assets.keys.each do |logical_path|
173
- # Get assets sorted by ctime, newest first
174
- assets = backups_for(logical_path)
263
+ # keep the latest version, 2 backups and any created within the past hour.
264
+ #
265
+ # Examples
266
+ #
267
+ # To force only 1 backup to be kept, set count=1 and age=0.
268
+ #
269
+ # To only keep files created within the last 10 minutes, set count=0 and
270
+ # age=600.
271
+ #
272
+ def clean(count = 2, age = 3600)
273
+ asset_versions = files.group_by { |_, attrs| attrs['logical_path'] }
175
274
 
176
- # Keep the last N backups
177
- assets = assets[keep..-1] || []
275
+ asset_versions.each do |logical_path, versions|
276
+ current = assets[logical_path]
178
277
 
179
- # Remove old assets
180
- assets.each { |path, _| remove(path) }
278
+ versions.reject { |path, _|
279
+ path == current
280
+ }.sort_by { |_, attrs|
281
+ # Sort by timestamp
282
+ Time.parse(attrs['mtime'])
283
+ }.reverse.each_with_index.drop_while { |(_, attrs), index|
284
+ _age = [0, Time.now - Time.parse(attrs['mtime'])].max
285
+ # Keep if under age or within the count limit
286
+ _age < age || index < count
287
+ }.each { |(path, _), _|
288
+ # Remove old assets
289
+ remove(path)
290
+ }
181
291
  end
182
292
  end
183
293
 
184
294
  # Wipe directive
185
295
  def clobber
186
- FileUtils.rm_r(@dir) if File.exist?(@dir)
187
- logger.info "Removed #{@dir}"
296
+ FileUtils.rm_r(directory) if File.exist?(directory)
297
+ logger.info "Removed #{directory}"
188
298
  nil
189
299
  end
190
300
 
191
- protected
192
- # Finds all the backup assets for a logical path. The latest
193
- # version is always excluded. The return array is sorted by the
194
- # assets mtime in descending order (Newest to oldest).
195
- def backups_for(logical_path)
196
- files.select { |filename, attrs|
197
- # Matching logical paths
198
- attrs['logical_path'] == logical_path &&
199
- # Excluding whatever asset is the current
200
- assets[logical_path] != filename
201
- }.sort_by { |filename, attrs|
202
- # Sort by timestamp
203
- Time.parse(attrs['mtime'])
204
- }.reverse
301
+ # Persist manfiest back to FS
302
+ 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
205
308
  end
206
309
 
207
- # Basic wrapper around Environment#find_asset. Logs compile time.
208
- def find_asset(logical_path)
209
- asset = nil
210
- ms = benchmark do
211
- asset = environment.find_asset(logical_path)
212
- end
213
- logger.debug "Compiled #{logical_path} (#{ms}ms)"
214
- asset
215
- end
216
-
217
- # Persist manfiest back to FS
218
- def save
219
- FileUtils.mkdir_p File.dirname(path)
220
- File.open(path, 'w') do |f|
221
- f.write json_encode(@data)
222
- end
310
+ data = json_encode(@data)
311
+ FileUtils.mkdir_p File.dirname(@filename)
312
+ PathUtils.atomic_write(@filename) do |f|
313
+ f.write(data)
223
314
  end
315
+ end
224
316
 
225
317
  private
226
- # Feature detect newer MultiJson API
227
- if MultiJson.respond_to?(:dump)
228
- def json_decode(obj)
229
- MultiJson.load(obj)
230
- end
231
-
232
- def json_encode(obj)
233
- MultiJson.dump(obj)
234
- end
235
- else
236
- def json_decode(obj)
237
- MultiJson.decode(obj)
238
- end
318
+ def json_decode(obj)
319
+ JSON.parse(obj, create_additions: false)
320
+ end
239
321
 
240
- def json_encode(obj)
241
- MultiJson.encode(obj)
242
- end
322
+ def json_encode(obj)
323
+ JSON.generate(obj)
243
324
  end
244
325
 
245
326
  def logger
@@ -251,11 +332,5 @@ module Sprockets
251
332
  logger
252
333
  end
253
334
  end
254
-
255
- def benchmark
256
- start_time = Time.now.to_f
257
- yield
258
- ((Time.now.to_f - start_time) * 1000).to_i
259
- end
260
335
  end
261
336
  end
@@ -0,0 +1,45 @@
1
+ require 'securerandom'
2
+
3
+ module Sprockets
4
+ # Public: Manifest utilities.
5
+ module ManifestUtils
6
+ extend self
7
+
8
+ MANIFEST_RE = /^\.sprockets-manifest-[0-9a-f]{32}.json$/
9
+ LEGACY_MANIFEST_RE = /^manifest(-[0-9a-f]{32})?.json$/
10
+
11
+ # Public: Generate a new random manifest path.
12
+ #
13
+ # Manifests are not intended to be accessed publicly, but typically live
14
+ # alongside public assets for convenience. To avoid being served, the
15
+ # filename is prefixed with a "." which is usually hidden by web servers
16
+ # like Apache. To help in other environments that may not control this,
17
+ # a random hex string is appended to the filename to prevent people from
18
+ # guessing the location. If directory indexes are enabled on the server,
19
+ # all bets are off.
20
+ #
21
+ # Return String path.
22
+ def generate_manifest_path
23
+ ".sprockets-manifest-#{SecureRandom.hex(16)}.json"
24
+ end
25
+
26
+ # Public: Find or pick a new manifest filename for target build directory.
27
+ #
28
+ # dirname - String dirname
29
+ #
30
+ # Examples
31
+ #
32
+ # find_directory_manifest("/app/public/assets")
33
+ # # => "/app/public/assets/.sprockets-manifest-abc123.json"
34
+ #
35
+ # Returns String filename.
36
+ def find_directory_manifest(dirname)
37
+ entries = File.directory?(dirname) ? Dir.entries(dirname) : []
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
+ generate_manifest_path
42
+ File.join(dirname, entry)
43
+ end
44
+ end
45
+ end
@@ -1,49 +1,130 @@
1
- require 'rack/mime'
1
+ require 'sprockets/encoding_utils'
2
+ require 'sprockets/http_utils'
3
+ require 'sprockets/utils'
2
4
 
3
5
  module Sprockets
4
6
  module Mime
5
- # Returns a `Hash` of registered mime types registered on the
6
- # environment and those part of `Rack::Mime`.
7
+ include HTTPUtils, Utils
8
+
9
+ # Public: Mapping of MIME type Strings to properties Hash.
7
10
  #
8
- # If an `ext` is given, it will lookup the mime type for that extension.
9
- def mime_types(ext = nil)
10
- if ext.nil?
11
- Rack::Mime::MIME_TYPES.merge(@mime_types)
12
- else
13
- ext = Sprockets::Utils.normalize_extension(ext)
14
- @mime_types[ext] || Rack::Mime::MIME_TYPES[ext]
15
- end
11
+ # key - MIME Type String
12
+ # value - Hash
13
+ # extensions - Array of extnames
14
+ # charset - Default Encoding or function to detect encoding
15
+ #
16
+ # Returns Hash.
17
+ def mime_types
18
+ config[:mime_types]
16
19
  end
17
20
 
18
- # Returns a `Hash` of explicitly registered mime types.
19
- def registered_mime_types
20
- @mime_types.dup
21
+ # Internal: Mapping of MIME extension Strings to MIME type Strings.
22
+ #
23
+ # Used for internal fast lookup purposes.
24
+ #
25
+ # Examples:
26
+ #
27
+ # mime_exts['.js'] #=> 'application/javascript'
28
+ #
29
+ # key - MIME extension String
30
+ # value - MIME Type String
31
+ #
32
+ # Returns Hash.
33
+ def mime_exts
34
+ config[:mime_exts]
21
35
  end
22
36
 
23
- if {}.respond_to?(:key)
24
- def extension_for_mime_type(type)
25
- mime_types.key(type)
37
+ # Public: Register a new mime type.
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.
44
+ #
45
+ # 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
+ }
55
+
56
+ charset = options[:charset]
57
+ charset ||= :default if mime_type.start_with?('text/')
58
+ charset = EncodingUtils::CHARSET_DETECT[charset] if charset.is_a?(Symbol)
59
+
60
+ self.computed_config = {}
61
+
62
+ self.config = hash_reassoc(config, :mime_exts) do |mime_exts|
63
+ extnames.each do |extname|
64
+ mime_exts[extname] = mime_type
65
+ end
66
+ mime_exts
26
67
  end
27
- else
28
- def extension_for_mime_type(type)
29
- mime_types.index(type)
68
+
69
+ self.config = hash_reassoc(config, :mime_types) do |mime_types|
70
+ type = { extensions: extnames }
71
+ type[:charset] = charset if charset
72
+ mime_types.merge(mime_type => type)
30
73
  end
31
74
  end
32
75
 
33
- # Register a new mime type.
34
- def register_mime_type(mime_type, ext)
35
- ext = Sprockets::Utils.normalize_extension(ext)
36
- @mime_types[ext] = mime_type
76
+ # Internal: Get detecter function for MIME type.
77
+ #
78
+ # mime_type - String MIME type
79
+ #
80
+ # Returns Proc detector or nil if none is available.
81
+ def mime_type_charset_detecter(mime_type)
82
+ if type = config[:mime_types][mime_type]
83
+ if detect = type[:charset]
84
+ return detect
85
+ end
86
+ end
37
87
  end
38
88
 
39
- if defined? Encoding
40
- # Returns the correct encoding for a given mime type, while falling
41
- # back on the default external encoding, if it exists.
42
- def encoding_for_mime_type(type)
43
- encoding = Encoding::BINARY if type =~ %r{^(image|audio|video)/}
44
- encoding ||= default_external_encoding if respond_to?(:default_external_encoding)
45
- encoding
89
+ # Public: Read file on disk with MIME type specific encoding.
90
+ #
91
+ # filename - String path
92
+ # content_type - String MIME type
93
+ #
94
+ # Returns String file contents transcoded to UTF-8 or in its external
95
+ # encoding.
96
+ def read_file(filename, content_type = nil)
97
+ data = File.binread(filename)
98
+
99
+ if detect = mime_type_charset_detecter(content_type)
100
+ detect.call(data).encode(Encoding::UTF_8, :universal_newline => true)
101
+ else
102
+ data
46
103
  end
47
104
  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
+ ([nil] + pipelines.keys.map(&:to_s)).each do |pipeline|
115
+ pipeline_extname = ".#{pipeline}" if pipeline
116
+ ([[nil, nil]] + config[:mime_exts].to_a).each do |format_extname, format_type|
117
+ 4.times do |n|
118
+ config[:engines].keys.permutation(n).each do |engine_extnames|
119
+ key = "#{pipeline_extname}#{format_extname}#{engine_extnames.join}"
120
+ type = format_type || config[:engine_mime_types][engine_extnames.first]
121
+ graph[key] = {type: type, engines: engine_extnames, pipeline: pipeline}
122
+ end
123
+ end
124
+ end
125
+ end
126
+
127
+ graph
128
+ end
48
129
  end
49
130
  end