sprockets 3.7.2 → 4.0.0

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 (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