sprockets 2.12.5 → 3.7.2

Sign up to get free protection for your applications and to get access to all the features.
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