sprockets 3.7.2 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -270
  3. data/README.md +443 -320
  4. data/bin/sprockets +11 -7
  5. data/lib/rake/sprocketstask.rb +3 -2
  6. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  7. data/lib/sprockets/asset.rb +16 -21
  8. data/lib/sprockets/autoload/babel.rb +8 -0
  9. data/lib/sprockets/autoload/closure.rb +1 -0
  10. data/lib/sprockets/autoload/coffee_script.rb +1 -0
  11. data/lib/sprockets/autoload/eco.rb +1 -0
  12. data/lib/sprockets/autoload/ejs.rb +1 -0
  13. data/lib/sprockets/autoload/jsminc.rb +8 -0
  14. data/lib/sprockets/autoload/sass.rb +1 -0
  15. data/lib/sprockets/autoload/sassc.rb +8 -0
  16. data/lib/sprockets/autoload/uglifier.rb +1 -0
  17. data/lib/sprockets/autoload/yui.rb +1 -0
  18. data/lib/sprockets/autoload/zopfli.rb +7 -0
  19. data/lib/sprockets/autoload.rb +5 -0
  20. data/lib/sprockets/babel_processor.rb +66 -0
  21. data/lib/sprockets/base.rb +47 -10
  22. data/lib/sprockets/bower.rb +5 -2
  23. data/lib/sprockets/bundle.rb +40 -4
  24. data/lib/sprockets/cache/file_store.rb +25 -3
  25. data/lib/sprockets/cache/memory_store.rb +9 -0
  26. data/lib/sprockets/cache/null_store.rb +8 -0
  27. data/lib/sprockets/cache.rb +36 -1
  28. data/lib/sprockets/cached_environment.rb +14 -19
  29. data/lib/sprockets/closure_compressor.rb +1 -0
  30. data/lib/sprockets/coffee_script_processor.rb +18 -4
  31. data/lib/sprockets/compressing.rb +43 -3
  32. data/lib/sprockets/configuration.rb +3 -7
  33. data/lib/sprockets/context.rb +97 -24
  34. data/lib/sprockets/dependencies.rb +1 -0
  35. data/lib/sprockets/digest_utils.rb +25 -5
  36. data/lib/sprockets/directive_processor.rb +45 -35
  37. data/lib/sprockets/eco_processor.rb +1 -0
  38. data/lib/sprockets/ejs_processor.rb +1 -0
  39. data/lib/sprockets/encoding_utils.rb +1 -0
  40. data/lib/sprockets/environment.rb +9 -4
  41. data/lib/sprockets/erb_processor.rb +28 -21
  42. data/lib/sprockets/errors.rb +1 -0
  43. data/lib/sprockets/exporters/base.rb +72 -0
  44. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  45. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  46. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  47. data/lib/sprockets/exporting.rb +73 -0
  48. data/lib/sprockets/file_reader.rb +1 -0
  49. data/lib/sprockets/http_utils.rb +25 -7
  50. data/lib/sprockets/jsminc_compressor.rb +32 -0
  51. data/lib/sprockets/jst_processor.rb +11 -10
  52. data/lib/sprockets/loader.rb +85 -67
  53. data/lib/sprockets/manifest.rb +64 -62
  54. data/lib/sprockets/manifest_utils.rb +9 -6
  55. data/lib/sprockets/mime.rb +8 -42
  56. data/lib/sprockets/npm.rb +52 -0
  57. data/lib/sprockets/path_dependency_utils.rb +3 -11
  58. data/lib/sprockets/path_digest_utils.rb +2 -1
  59. data/lib/sprockets/path_utils.rb +87 -7
  60. data/lib/sprockets/paths.rb +1 -0
  61. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  62. data/lib/sprockets/processing.rb +31 -61
  63. data/lib/sprockets/processor_utils.rb +24 -35
  64. data/lib/sprockets/resolve.rb +177 -93
  65. data/lib/sprockets/sass_cache_store.rb +2 -6
  66. data/lib/sprockets/sass_compressor.rb +13 -1
  67. data/lib/sprockets/sass_functions.rb +1 -0
  68. data/lib/sprockets/sass_importer.rb +1 -0
  69. data/lib/sprockets/sass_processor.rb +30 -9
  70. data/lib/sprockets/sassc_compressor.rb +56 -0
  71. data/lib/sprockets/sassc_processor.rb +297 -0
  72. data/lib/sprockets/server.rb +26 -23
  73. data/lib/sprockets/source_map_processor.rb +66 -0
  74. data/lib/sprockets/source_map_utils.rb +483 -0
  75. data/lib/sprockets/transformers.rb +63 -35
  76. data/lib/sprockets/uglifier_compressor.rb +21 -11
  77. data/lib/sprockets/unloaded_asset.rb +13 -11
  78. data/lib/sprockets/uri_tar.rb +1 -0
  79. data/lib/sprockets/uri_utils.rb +11 -8
  80. data/lib/sprockets/utils/gzip.rb +46 -14
  81. data/lib/sprockets/utils.rb +41 -74
  82. data/lib/sprockets/version.rb +2 -1
  83. data/lib/sprockets/yui_compressor.rb +1 -0
  84. data/lib/sprockets.rb +99 -39
  85. metadata +127 -23
  86. data/LICENSE +0 -21
  87. data/lib/sprockets/coffee_script_template.rb +0 -17
  88. data/lib/sprockets/deprecation.rb +0 -90
  89. data/lib/sprockets/eco_template.rb +0 -17
  90. data/lib/sprockets/ejs_template.rb +0 -17
  91. data/lib/sprockets/engines.rb +0 -92
  92. data/lib/sprockets/erb_template.rb +0 -11
  93. data/lib/sprockets/legacy.rb +0 -330
  94. data/lib/sprockets/legacy_proc_processor.rb +0 -35
  95. data/lib/sprockets/legacy_tilt_processor.rb +0 -29
  96. data/lib/sprockets/sass_template.rb +0 -19
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Sprockets
2
3
  class Cache
3
4
  # Public: A compatible cache store that doesn't store anything. Used by
@@ -41,6 +42,13 @@ module Sprockets
41
42
  def inspect
42
43
  "#<#{self.class}>"
43
44
  end
45
+
46
+ # Public: Simulate clearing the cache
47
+ #
48
+ # Returns true
49
+ def clear(options=nil)
50
+ true
51
+ end
44
52
  end
45
53
  end
46
54
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'logger'
2
3
  require 'sprockets/digest_utils'
3
4
 
@@ -35,6 +36,12 @@ module Sprockets
35
36
  #
36
37
  # Returns argument value.
37
38
  #
39
+ # clear(options)
40
+ #
41
+ # Clear the entire cache. Be careful with this method since it could
42
+ # affect other processes if shared cache is being used.
43
+ #
44
+ # The options hash is passed to the underlying cache implementation.
38
45
  class Cache
39
46
  # Builtin cache stores.
40
47
  autoload :FileStore, 'sprockets/cache/file_store'
@@ -44,7 +51,7 @@ module Sprockets
44
51
  # Internal: Cache key version for this class. Rarely should have to change
45
52
  # unless the cache format radically changes. Will be bump on major version
46
53
  # releases though.
47
- VERSION = '3.0'
54
+ VERSION = '4.0.0'
48
55
 
49
56
  def self.default_logger
50
57
  logger = Logger.new($stderr)
@@ -143,6 +150,14 @@ module Sprockets
143
150
  "#<#{self.class} local=#{@fetch_cache.inspect} store=#{@cache_wrapper.cache.inspect}>"
144
151
  end
145
152
 
153
+ # Public: Clear cache
154
+ #
155
+ # Returns truthy on success, potentially raises exception on failure
156
+ def clear(options=nil)
157
+ @cache_wrapper.clear
158
+ @fetch_cache.clear
159
+ end
160
+
146
161
  private
147
162
  # Internal: Expand object cache key into a short String key.
148
163
  #
@@ -211,6 +226,16 @@ module Sprockets
211
226
  def set(key, value)
212
227
  cache.set(key, value)
213
228
  end
229
+
230
+ def clear(options=nil)
231
+ # dalli has a #flush method so try it
232
+ if cache.respond_to?(:flush)
233
+ cache.flush(options)
234
+ else
235
+ cache.clear(options)
236
+ end
237
+ true
238
+ end
214
239
  end
215
240
 
216
241
  class HashWrapper < Wrapper
@@ -221,6 +246,11 @@ module Sprockets
221
246
  def set(key, value)
222
247
  cache[key] = value
223
248
  end
249
+
250
+ def clear(options=nil)
251
+ cache.clear
252
+ true
253
+ end
224
254
  end
225
255
 
226
256
  class ReadWriteWrapper < Wrapper
@@ -231,6 +261,11 @@ module Sprockets
231
261
  def set(key, value)
232
262
  cache.write(key, value)
233
263
  end
264
+
265
+ def clear(options=nil)
266
+ cache.clear(options)
267
+ true
268
+ end
234
269
  end
235
270
  end
236
271
  end
@@ -1,26 +1,26 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/base'
2
3
 
3
4
  module Sprockets
4
- # `Cached` is a special cached version of `Environment`.
5
+ # `CachedEnvironment` is a special cached version of `Environment`.
5
6
  #
6
7
  # The expection is that all of its file system methods are cached
7
- # for the instances lifetime. This makes `Cached` much faster. This
8
+ # for the instances lifetime. This makes `CachedEnvironment` much faster. This
8
9
  # behavior is ideal in production environments where the file system
9
10
  # is immutable.
10
11
  #
11
- # `Cached` should not be initialized directly. Instead use
12
+ # `CachedEnvironment` should not be initialized directly. Instead use
12
13
  # `Environment#cached`.
13
14
  class CachedEnvironment < Base
14
15
  def initialize(environment)
15
16
  initialize_configuration(environment)
16
17
 
17
18
  @cache = environment.cache
18
- @stats = Hash.new { |h, k| h[k] = _stat(k) }
19
- @entries = Hash.new { |h, k| h[k] = _entries(k) }
20
- @uris = Hash.new { |h, k| h[k] = _load(k) }
21
-
22
- @processor_cache_keys = Hash.new { |h, k| h[k] = _processor_cache_key(k) }
23
- @resolved_dependencies = Hash.new { |h, k| h[k] = _resolve_dependency(k) }
19
+ @stats = {}
20
+ @entries = {}
21
+ @uris = {}
22
+ @processor_cache_keys = {}
23
+ @resolved_dependencies = {}
24
24
  end
25
25
 
26
26
  # No-op return self as cached environment.
@@ -30,33 +30,28 @@ module Sprockets
30
30
  alias_method :index, :cached
31
31
 
32
32
  # Internal: Cache Environment#entries
33
- alias_method :_entries, :entries
34
33
  def entries(path)
35
- @entries[path]
34
+ @entries[path] ||= super(path)
36
35
  end
37
36
 
38
37
  # Internal: Cache Environment#stat
39
- alias_method :_stat, :stat
40
38
  def stat(path)
41
- @stats[path]
39
+ @stats[path] ||= super(path)
42
40
  end
43
41
 
44
42
  # Internal: Cache Environment#load
45
- alias_method :_load, :load
46
43
  def load(uri)
47
- @uris[uri]
44
+ @uris[uri] ||= super(uri)
48
45
  end
49
46
 
50
47
  # Internal: Cache Environment#processor_cache_key
51
- alias_method :_processor_cache_key, :processor_cache_key
52
48
  def processor_cache_key(str)
53
- @processor_cache_keys[str]
49
+ @processor_cache_keys[str] ||= super(str)
54
50
  end
55
51
 
56
52
  # Internal: Cache Environment#resolve_dependency
57
- alias_method :_resolve_dependency, :resolve_dependency
58
53
  def resolve_dependency(str)
59
- @resolved_dependencies[str]
54
+ @resolved_dependencies[str] ||= super(str)
60
55
  end
61
56
 
62
57
  private
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/autoload'
2
3
  require 'sprockets/digest_utils'
3
4
 
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/autoload'
3
+ require 'sprockets/source_map_utils'
2
4
 
3
5
  module Sprockets
4
6
  # Processor engine class for the CoffeeScript compiler.
@@ -6,10 +8,10 @@ module Sprockets
6
8
  #
7
9
  # For more infomation see:
8
10
  #
9
- # https://github.com/josh/ruby-coffee-script
11
+ # https://github.com/rails/ruby-coffee-script
10
12
  #
11
13
  module CoffeeScriptProcessor
12
- VERSION = '1'
14
+ VERSION = '2'
13
15
 
14
16
  def self.cache_key
15
17
  @cache_key ||= "#{name}:#{Autoload::CoffeeScript::Source.version}:#{VERSION}".freeze
@@ -17,9 +19,21 @@ module Sprockets
17
19
 
18
20
  def self.call(input)
19
21
  data = input[:data]
20
- input[:cache].fetch([self.cache_key, data]) do
21
- Autoload::CoffeeScript.compile(data)
22
+
23
+ js, map = input[:cache].fetch([self.cache_key, data]) do
24
+ result = Autoload::CoffeeScript.compile(
25
+ data,
26
+ sourceMap: "v3",
27
+ sourceFiles: [File.basename(input[:filename])],
28
+ generatedFile: input[:filename]
29
+ )
30
+ [result['js'], JSON.parse(result['v3SourceMap'])]
22
31
  end
32
+
33
+ map = SourceMapUtils.format_source_map(map, input)
34
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
35
+
36
+ { data: js, map: map }
23
37
  end
24
38
  end
25
39
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/utils'
2
3
 
3
4
  module Sprockets
@@ -10,6 +11,24 @@ module Sprockets
10
11
  config[:compressors]
11
12
  end
12
13
 
14
+ # Public: Register a new compressor `klass` at `sym` for `mime_type`.
15
+ #
16
+ # Registering a processor allows it to be looked up by `sym` later when
17
+ # assigning a JavaScript or CSS compressor.
18
+ #
19
+ # Compressors only operate on JavaScript and CSS. If you want to compress a
20
+ # different type of asset, use a processor instead.
21
+ #
22
+ # Examples
23
+ #
24
+ # register_compressor 'text/css', :my_sass, MySassCompressor
25
+ # css_compressor = :my_sass
26
+ #
27
+ # mime_type - String MIME Type (one of: 'test/css' or 'application/javascript').
28
+ # sym - Symbol registration address.
29
+ # klass - The compressor class.
30
+ #
31
+ # Returns nothing.
13
32
  def register_compressor(mime_type, sym, klass)
14
33
  self.config = hash_reassoc(config, :compressors, mime_type) do |compressors|
15
34
  compressors[sym] = klass
@@ -35,7 +54,7 @@ module Sprockets
35
54
  if compressor.is_a?(Symbol)
36
55
  @css_compressor = klass = config[:compressors]['text/css'][compressor] || raise(Error, "unknown compressor: #{compressor}")
37
56
  elsif compressor.respond_to?(:compress)
38
- klass = LegacyProcProcessor.new(:css_compressor, proc { |context, data| compressor.compress(data) })
57
+ klass = proc { |input| compressor.compress(input[:data]) }
39
58
  @css_compressor = :css_compressor
40
59
  else
41
60
  @css_compressor = klass = compressor
@@ -62,7 +81,7 @@ module Sprockets
62
81
  if compressor.is_a?(Symbol)
63
82
  @js_compressor = klass = config[:compressors]['application/javascript'][compressor] || raise(Error, "unknown compressor: #{compressor}")
64
83
  elsif compressor.respond_to?(:compress)
65
- klass = LegacyProcProcessor.new(:js_compressor, proc { |context, data| compressor.compress(data) })
84
+ klass = proc { |input| compressor.compress(input[:data]) }
66
85
  @js_compressor = :js_compressor
67
86
  else
68
87
  @js_compressor = klass = compressor
@@ -83,12 +102,33 @@ module Sprockets
83
102
 
84
103
  # Public: Enable or disable the creation of Gzip files.
85
104
  #
86
- # Defaults to true.
105
+ # To disable gzip generation set to a falsey value:
87
106
  #
88
107
  # environment.gzip = false
89
108
  #
109
+ # To enable set to a truthy value. By default zlib wil
110
+ # be used to gzip assets. If you have the Zopfli gem
111
+ # installed you can specify the zopfli algorithm to be used
112
+ # instead:
113
+ #
114
+ # environment.gzip = :zopfli
115
+ #
90
116
  def gzip=(gzip)
91
117
  self.config = config.merge(gzip_enabled: gzip).freeze
118
+
119
+ case gzip
120
+ when false, nil
121
+ self.unregister_exporter Exporters::ZlibExporter
122
+ self.unregister_exporter Exporters::ZopfliExporter
123
+ when :zopfli
124
+ self.unregister_exporter Exporters::ZlibExporter
125
+ self.register_exporter '*/*', Exporters::ZopfliExporter
126
+ else
127
+ self.unregister_exporter Exporters::ZopfliExporter
128
+ self.register_exporter '*/*', Exporters::ZlibExporter
129
+ end
130
+
131
+ gzip
92
132
  end
93
133
  end
94
134
  end
@@ -1,27 +1,25 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/compressing'
2
3
  require 'sprockets/dependencies'
3
- require 'sprockets/engines'
4
4
  require 'sprockets/mime'
5
5
  require 'sprockets/paths'
6
6
  require 'sprockets/processing'
7
+ require 'sprockets/exporting'
7
8
  require 'sprockets/transformers'
8
9
  require 'sprockets/utils'
9
10
 
10
11
  module Sprockets
11
12
  module Configuration
12
- include Paths, Mime, Engines, Transformers, Processing, Compressing, Dependencies, Utils
13
+ include Paths, Mime, Transformers, Processing, Exporting, Compressing, Dependencies, Utils
13
14
 
14
15
  def initialize_configuration(parent)
15
16
  @config = parent.config
16
- @computed_config = parent.computed_config
17
17
  @logger = parent.logger
18
18
  @context_class = Class.new(parent.context_class)
19
19
  end
20
20
 
21
21
  attr_reader :config
22
22
 
23
- attr_accessor :computed_config
24
-
25
23
  def config=(config)
26
24
  raise TypeError, "can't assign mutable config" unless config.frozen?
27
25
  @config = config
@@ -69,8 +67,6 @@ module Sprockets
69
67
  self.config = config.merge(digest_class: klass).freeze
70
68
  end
71
69
 
72
- # Deprecated: Get `Context` class.
73
- #
74
70
  # This class maybe mutated and mixed in with custom helpers.
75
71
  #
76
72
  # environment.context_class.instance_eval do
@@ -1,10 +1,9 @@
1
- require 'pathname'
1
+ # frozen_string_literal: true
2
2
  require 'rack/utils'
3
3
  require 'set'
4
4
  require 'sprockets/errors'
5
5
 
6
6
  module Sprockets
7
- # Deprecated: `Context` provides helper methods to all processors.
8
7
  # They are typically accessed by ERB templates. You can mix in custom helpers
9
8
  # by injecting them into `Environment#context_class`. Do not mix them into
10
9
  # `Context` directly.
@@ -19,10 +18,25 @@ module Sprockets
19
18
  # The `Context` also collects dependencies declared by
20
19
  # assets. See `DirectiveProcessor` for an example of this.
21
20
  class Context
22
- attr_reader :environment, :filename, :pathname
21
+ # Internal: Proxy for ENV that keeps track of the environment variables used
22
+ class ENVProxy < SimpleDelegator
23
+ def initialize(context)
24
+ @context = context
25
+ super(ENV)
26
+ end
23
27
 
24
- # Deprecated
25
- attr_accessor :__LINE__
28
+ def [](key)
29
+ @context.depend_on_env(key)
30
+ super
31
+ end
32
+
33
+ def fetch(key, *)
34
+ @context.depend_on_env(key)
35
+ super
36
+ end
37
+ end
38
+
39
+ attr_reader :environment, :filename
26
40
 
27
41
  def initialize(input)
28
42
  @environment = input[:environment]
@@ -31,7 +45,6 @@ module Sprockets
31
45
  @logical_path = input[:name]
32
46
  @filename = input[:filename]
33
47
  @dirname = File.dirname(@filename)
34
- @pathname = Pathname.new(@filename)
35
48
  @content_type = input[:content_type]
36
49
 
37
50
  @required = Set.new(@metadata[:required])
@@ -47,6 +60,10 @@ module Sprockets
47
60
  dependencies: @dependencies }
48
61
  end
49
62
 
63
+ def env_proxy
64
+ ENVProxy.new(self)
65
+ end
66
+
50
67
  # Returns the environment path that contains the file.
51
68
  #
52
69
  # If `app/javascripts` and `app/stylesheets` are in your path, and
@@ -79,13 +96,13 @@ module Sprockets
79
96
  # resolve("./bar.js")
80
97
  # # => "file:///path/to/app/javascripts/bar.js?type=application/javascript"
81
98
  #
82
- # path - String logical or absolute path
83
- # options
84
- # accept - String content accept type
99
+ # path - String logical or absolute path
100
+ # accept - String content accept type
85
101
  #
86
102
  # Returns an Asset URI String.
87
- def resolve(path, options = {})
88
- uri, deps = environment.resolve!(path, options.merge(base_path: @dirname))
103
+ def resolve(path, **kargs)
104
+ kargs[:base_path] = @dirname
105
+ uri, deps = environment.resolve!(path, **kargs)
89
106
  @dependencies.merge(deps)
90
107
  uri
91
108
  end
@@ -105,15 +122,13 @@ module Sprockets
105
122
  # including it.
106
123
  #
107
124
  # This is used for caching purposes. Any changes made to
108
- # the dependency file with invalidate the cache of the
125
+ # the dependency file will invalidate the cache of the
109
126
  # source file.
110
127
  def depend_on(path)
111
- path = path.to_s if path.is_a?(Pathname)
112
-
113
128
  if environment.absolute_path?(path) && environment.stat(path)
114
129
  @dependencies << environment.build_file_digest_uri(path)
115
130
  else
116
- resolve(path, compat: false)
131
+ resolve(path)
117
132
  end
118
133
  nil
119
134
  end
@@ -123,10 +138,19 @@ module Sprockets
123
138
  #
124
139
  # This is used for caching purposes. Any changes that would
125
140
  # invalidate the dependency asset will invalidate the source
126
- # file. Unlike `depend_on`, this will include recursively include
141
+ # file. Unlike `depend_on`, this will recursively include
127
142
  # the target asset's dependencies.
128
143
  def depend_on_asset(path)
129
- load(resolve(path, compat: false))
144
+ load(resolve(path))
145
+ end
146
+
147
+ # `depend_on_env` allows you to state a dependency on an environment
148
+ # variable.
149
+ #
150
+ # This is used for caching purposes. Any changes in the value of the
151
+ # environment variable will invalidate the cache of the source file.
152
+ def depend_on_env(key)
153
+ @dependencies << "env:#{key}"
130
154
  end
131
155
 
132
156
  # `require_asset` declares `path` as a dependency of the file. The
@@ -139,7 +163,7 @@ module Sprockets
139
163
  # <%= require_asset "#{framework}.js" %>
140
164
  #
141
165
  def require_asset(path)
142
- @required << resolve(path, accept: @content_type, pipeline: :self, compat: false)
166
+ @required << resolve(path, accept: @content_type, pipeline: :self)
143
167
  nil
144
168
  end
145
169
 
@@ -147,7 +171,7 @@ module Sprockets
147
171
  # `path` must be an asset which may or may not already be included
148
172
  # in the bundle.
149
173
  def stub_asset(path)
150
- @stubbed << resolve(path, accept: @content_type, pipeline: :self, compat: false)
174
+ @stubbed << resolve(path, accept: @content_type, pipeline: :self)
151
175
  nil
152
176
  end
153
177
 
@@ -162,9 +186,10 @@ module Sprockets
162
186
  asset
163
187
  end
164
188
 
165
- # Returns a Base64-encoded `data:` URI with the contents of the
166
- # asset at the specified path, and marks that path as a dependency
167
- # of the current file.
189
+ # Returns a `data:` URI with the contents of the asset at the specified
190
+ # path, and marks that path as a dependency of the current file.
191
+ #
192
+ # Uses URI encoding for SVG files, base64 encoding for all the other files.
168
193
  #
169
194
  # Use `asset_data_uri` from ERB with CSS or JavaScript assets:
170
195
  #
@@ -174,8 +199,11 @@ module Sprockets
174
199
  #
175
200
  def asset_data_uri(path)
176
201
  asset = depend_on_asset(path)
177
- data = EncodingUtils.base64(asset.source)
178
- "data:#{asset.content_type};base64,#{Rack::Utils.escape(data)}"
202
+ if asset.content_type == 'image/svg+xml'
203
+ svg_asset_data_uri(asset)
204
+ else
205
+ base64_asset_data_uri(asset)
206
+ end
179
207
  end
180
208
 
181
209
  # Expands logical path to full url to asset.
@@ -227,5 +255,50 @@ Extend your environment context with a custom method.
227
255
  def stylesheet_path(path)
228
256
  asset_path(path, type: :stylesheet)
229
257
  end
258
+
259
+ protected
260
+
261
+ # Returns a URI-encoded data URI (always "-quoted).
262
+ def svg_asset_data_uri(asset)
263
+ svg = asset.source.dup
264
+ optimize_svg_for_uri_escaping!(svg)
265
+ data = Rack::Utils.escape(svg)
266
+ optimize_quoted_uri_escapes!(data)
267
+ "\"data:#{asset.content_type};charset=utf-8,#{data}\""
268
+ end
269
+
270
+ # Returns a Base64-encoded data URI.
271
+ def base64_asset_data_uri(asset)
272
+ data = Rack::Utils.escape(EncodingUtils.base64(asset.source))
273
+ "data:#{asset.content_type};base64,#{data}"
274
+ end
275
+
276
+ # Optimizes an SVG for being URI-escaped.
277
+ #
278
+ # This method only performs these basic but crucial optimizations:
279
+ # * Replaces " with ', because ' does not need escaping.
280
+ # * Removes comments, meta, doctype, and newlines.
281
+ # * Collapses whitespace.
282
+ def optimize_svg_for_uri_escaping!(svg)
283
+ # Remove comments, xml meta, and doctype
284
+ svg.gsub!(/<!--.*?-->|<\?.*?\?>|<!.*?>/m, '')
285
+ # Replace consecutive whitespace and newlines with a space
286
+ svg.gsub!(/\s+/, ' ')
287
+ # Collapse inter-tag whitespace
288
+ svg.gsub!('> <', '><')
289
+ # Replace " with '
290
+ svg.gsub!(/([\w:])="(.*?)"/, "\\1='\\2'")
291
+ svg.strip!
292
+ end
293
+
294
+ # Un-escapes characters in the given URI-escaped string that do not need
295
+ # escaping in "-quoted data URIs.
296
+ def optimize_quoted_uri_escapes!(escaped)
297
+ escaped.gsub!('%3D', '=')
298
+ escaped.gsub!('%3A', ':')
299
+ escaped.gsub!('%2F', '/')
300
+ escaped.gsub!('%27', "'")
301
+ escaped.tr!('+', ' ')
302
+ end
230
303
  end
231
304
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'sprockets/digest_utils'
2
3
  require 'sprockets/path_digest_utils'
3
4
  require 'sprockets/uri_utils'
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'digest/md5'
2
3
  require 'digest/sha1'
3
4
  require 'digest/sha2'
@@ -62,7 +63,7 @@ module Sprockets
62
63
  },
63
64
  Set => ->(val, digest) {
64
65
  digest << 'Set'.freeze
65
- ADD_VALUE_TO_DIGEST[Array].call(val.to_a, digest)
66
+ ADD_VALUE_TO_DIGEST[Array].call(val, digest)
66
67
  },
67
68
  Encoding => ->(val, digest) {
68
69
  digest << 'Encoding'.freeze
@@ -79,6 +80,9 @@ module Sprockets
79
80
  digest << val.to_s
80
81
  }
81
82
  end
83
+
84
+ ADD_VALUE_TO_DIGEST.compare_by_identity.rehash
85
+
82
86
  ADD_VALUE_TO_DIGEST.default_proc = ->(_, val) {
83
87
  raise TypeError, "couldn't digest #{ val }"
84
88
  }
@@ -93,10 +97,18 @@ module Sprockets
93
97
  #
94
98
  # Returns a String digest of the object.
95
99
  def digest(obj)
96
- digest = digest_class.new
100
+ build_digest(obj).digest
101
+ end
97
102
 
98
- ADD_VALUE_TO_DIGEST[obj.class].call(obj, digest)
99
- digest.digest
103
+ # Internal: Generate a hexdigest for a nested JSON serializable object.
104
+ #
105
+ # The same as `pack_hexdigest(digest(obj))`.
106
+ #
107
+ # obj - A JSON serializable object.
108
+ #
109
+ # Returns a String digest of the object.
110
+ def hexdigest(obj)
111
+ build_digest(obj).hexdigest!
100
112
  end
101
113
 
102
114
  # Internal: Pack a binary digest to a hex encoded string.
@@ -105,7 +117,7 @@ module Sprockets
105
117
  #
106
118
  # Returns hex String.
107
119
  def pack_hexdigest(bin)
108
- bin.unpack('H*').first
120
+ bin.unpack('H*'.freeze).first
109
121
  end
110
122
 
111
123
  # Internal: Unpack a hex encoded digest string into binary bytes.
@@ -176,5 +188,13 @@ module Sprockets
176
188
  def hexdigest_integrity_uri(hexdigest)
177
189
  integrity_uri(unpack_hexdigest(hexdigest))
178
190
  end
191
+
192
+ private
193
+ def build_digest(obj)
194
+ digest = digest_class.new
195
+
196
+ ADD_VALUE_TO_DIGEST[obj.class].call(obj, digest)
197
+ digest
198
+ end
179
199
  end
180
200
  end