sprockets 4.0.1

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 (85) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +72 -0
  3. data/README.md +665 -0
  4. data/bin/sprockets +93 -0
  5. data/lib/rake/sprocketstask.rb +153 -0
  6. data/lib/sprockets.rb +229 -0
  7. data/lib/sprockets/add_source_map_comment_to_asset_processor.rb +60 -0
  8. data/lib/sprockets/asset.rb +202 -0
  9. data/lib/sprockets/autoload.rb +16 -0
  10. data/lib/sprockets/autoload/babel.rb +8 -0
  11. data/lib/sprockets/autoload/closure.rb +8 -0
  12. data/lib/sprockets/autoload/coffee_script.rb +8 -0
  13. data/lib/sprockets/autoload/eco.rb +8 -0
  14. data/lib/sprockets/autoload/ejs.rb +8 -0
  15. data/lib/sprockets/autoload/jsminc.rb +8 -0
  16. data/lib/sprockets/autoload/sass.rb +8 -0
  17. data/lib/sprockets/autoload/sassc.rb +8 -0
  18. data/lib/sprockets/autoload/uglifier.rb +8 -0
  19. data/lib/sprockets/autoload/yui.rb +8 -0
  20. data/lib/sprockets/autoload/zopfli.rb +7 -0
  21. data/lib/sprockets/babel_processor.rb +66 -0
  22. data/lib/sprockets/base.rb +147 -0
  23. data/lib/sprockets/bower.rb +61 -0
  24. data/lib/sprockets/bundle.rb +105 -0
  25. data/lib/sprockets/cache.rb +271 -0
  26. data/lib/sprockets/cache/file_store.rb +208 -0
  27. data/lib/sprockets/cache/memory_store.rb +75 -0
  28. data/lib/sprockets/cache/null_store.rb +54 -0
  29. data/lib/sprockets/cached_environment.rb +64 -0
  30. data/lib/sprockets/closure_compressor.rb +48 -0
  31. data/lib/sprockets/coffee_script_processor.rb +39 -0
  32. data/lib/sprockets/compressing.rb +134 -0
  33. data/lib/sprockets/configuration.rb +79 -0
  34. data/lib/sprockets/context.rb +304 -0
  35. data/lib/sprockets/dependencies.rb +74 -0
  36. data/lib/sprockets/digest_utils.rb +200 -0
  37. data/lib/sprockets/directive_processor.rb +414 -0
  38. data/lib/sprockets/eco_processor.rb +33 -0
  39. data/lib/sprockets/ejs_processor.rb +32 -0
  40. data/lib/sprockets/encoding_utils.rb +262 -0
  41. data/lib/sprockets/environment.rb +46 -0
  42. data/lib/sprockets/erb_processor.rb +37 -0
  43. data/lib/sprockets/errors.rb +12 -0
  44. data/lib/sprockets/exporters/base.rb +71 -0
  45. data/lib/sprockets/exporters/file_exporter.rb +24 -0
  46. data/lib/sprockets/exporters/zlib_exporter.rb +33 -0
  47. data/lib/sprockets/exporters/zopfli_exporter.rb +14 -0
  48. data/lib/sprockets/exporting.rb +73 -0
  49. data/lib/sprockets/file_reader.rb +16 -0
  50. data/lib/sprockets/http_utils.rb +135 -0
  51. data/lib/sprockets/jsminc_compressor.rb +32 -0
  52. data/lib/sprockets/jst_processor.rb +50 -0
  53. data/lib/sprockets/loader.rb +345 -0
  54. data/lib/sprockets/manifest.rb +338 -0
  55. data/lib/sprockets/manifest_utils.rb +48 -0
  56. data/lib/sprockets/mime.rb +96 -0
  57. data/lib/sprockets/npm.rb +52 -0
  58. data/lib/sprockets/path_dependency_utils.rb +77 -0
  59. data/lib/sprockets/path_digest_utils.rb +48 -0
  60. data/lib/sprockets/path_utils.rb +367 -0
  61. data/lib/sprockets/paths.rb +82 -0
  62. data/lib/sprockets/preprocessors/default_source_map.rb +49 -0
  63. data/lib/sprockets/processing.rb +228 -0
  64. data/lib/sprockets/processor_utils.rb +169 -0
  65. data/lib/sprockets/resolve.rb +295 -0
  66. data/lib/sprockets/sass_cache_store.rb +30 -0
  67. data/lib/sprockets/sass_compressor.rb +63 -0
  68. data/lib/sprockets/sass_functions.rb +3 -0
  69. data/lib/sprockets/sass_importer.rb +3 -0
  70. data/lib/sprockets/sass_processor.rb +313 -0
  71. data/lib/sprockets/sassc_compressor.rb +56 -0
  72. data/lib/sprockets/sassc_processor.rb +297 -0
  73. data/lib/sprockets/server.rb +295 -0
  74. data/lib/sprockets/source_map_processor.rb +66 -0
  75. data/lib/sprockets/source_map_utils.rb +483 -0
  76. data/lib/sprockets/transformers.rb +173 -0
  77. data/lib/sprockets/uglifier_compressor.rb +66 -0
  78. data/lib/sprockets/unloaded_asset.rb +139 -0
  79. data/lib/sprockets/uri_tar.rb +99 -0
  80. data/lib/sprockets/uri_utils.rb +191 -0
  81. data/lib/sprockets/utils.rb +202 -0
  82. data/lib/sprockets/utils/gzip.rb +99 -0
  83. data/lib/sprockets/version.rb +4 -0
  84. data/lib/sprockets/yui_compressor.rb +56 -0
  85. metadata +444 -0
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+ require 'sprockets/autoload'
3
+ require 'sprockets/source_map_utils'
4
+
5
+ module Sprockets
6
+ # Public: Sass CSS minifier.
7
+ #
8
+ # To accept the default options
9
+ #
10
+ # environment.register_bundle_processor 'text/css',
11
+ # Sprockets::SasscCompressor
12
+ #
13
+ # Or to pass options to the Sass::Engine class.
14
+ #
15
+ # environment.register_bundle_processor 'text/css',
16
+ # Sprockets::SasscCompressor.new({ ... })
17
+ #
18
+ class SasscCompressor
19
+ # Public: Return singleton instance with default options.
20
+ #
21
+ # Returns SasscCompressor object.
22
+ def self.instance
23
+ @instance ||= new
24
+ end
25
+
26
+ def self.call(input)
27
+ instance.call(input)
28
+ end
29
+
30
+ def initialize(options = {})
31
+ @options = {
32
+ syntax: :scss,
33
+ style: :compressed,
34
+ source_map_contents: false,
35
+ omit_source_map_url: true,
36
+ }.merge(options).freeze
37
+ end
38
+
39
+ def call(input)
40
+ # SassC requires the template to be modifiable
41
+ input_data = input[:data].frozen? ? input[:data].dup : input[:data]
42
+ engine = Autoload::SassC::Engine.new(input_data, @options.merge(filename: input[:filename], source_map_file: "#{input[:filename]}.map"))
43
+
44
+ css = engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
45
+
46
+ begin
47
+ map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
48
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
49
+ rescue SassC::NotRenderedError
50
+ map = input[:metadata][:map]
51
+ end
52
+
53
+ { data: css, map: map }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+ require 'rack/utils'
3
+ require 'sprockets/autoload'
4
+ require 'sprockets/source_map_utils'
5
+ require 'uri'
6
+
7
+ module Sprockets
8
+ # Processor engine class for the SASS/SCSS compiler. Depends on the `sassc` gem.
9
+ #
10
+ # For more infomation see:
11
+ #
12
+ # https://github.com/sass/sassc-ruby
13
+ # https://github.com/sass/sassc-rails
14
+ #
15
+ class SasscProcessor
16
+
17
+ # Internal: Defines default sass syntax to use. Exposed so the ScsscProcessor
18
+ # may override it.
19
+ def self.syntax
20
+ :sass
21
+ end
22
+
23
+ # Public: Return singleton instance with default options.
24
+ #
25
+ # Returns SasscProcessor object.
26
+ def self.instance
27
+ @instance ||= new
28
+ end
29
+
30
+ def self.call(input)
31
+ instance.call(input)
32
+ end
33
+
34
+ def self.cache_key
35
+ instance.cache_key
36
+ end
37
+
38
+ attr_reader :cache_key
39
+
40
+ def initialize(options = {}, &block)
41
+ @cache_version = options[:cache_version]
42
+ @cache_key = "#{self.class.name}:#{VERSION}:#{Autoload::SassC::VERSION}:#{@cache_version}".freeze
43
+ @importer_class = options[:importer]
44
+ @sass_config = options[:sass_config] || {}
45
+ @functions = Module.new do
46
+ include Functions
47
+ include options[:functions] if options[:functions]
48
+ class_eval(&block) if block_given?
49
+ end
50
+ end
51
+
52
+ def call(input)
53
+ context = input[:environment].context_class.new(input)
54
+
55
+ options = engine_options(input, context)
56
+ engine = Autoload::SassC::Engine.new(input[:data], options)
57
+
58
+ css = Utils.module_include(Autoload::SassC::Script::Functions, @functions) do
59
+ engine.render.sub(/^\n^\/\*# sourceMappingURL=.*\*\/$/m, '')
60
+ end
61
+
62
+ begin
63
+ map = SourceMapUtils.format_source_map(JSON.parse(engine.source_map), input)
64
+ map = SourceMapUtils.combine_source_maps(input[:metadata][:map], map)
65
+
66
+ engine.dependencies.each do |dependency|
67
+ context.metadata[:dependencies] << URIUtils.build_file_digest_uri(dependency.filename)
68
+ end
69
+ rescue SassC::NotRenderedError
70
+ map = input[:metadata][:map]
71
+ end
72
+
73
+ context.metadata.merge(data: css, map: map)
74
+ end
75
+
76
+ private
77
+
78
+ def merge_options(options)
79
+ defaults = @sass_config.dup
80
+
81
+ if load_paths = defaults.delete(:load_paths)
82
+ options[:load_paths] += load_paths
83
+ end
84
+
85
+ options.merge!(defaults)
86
+ options
87
+ end
88
+
89
+ # Public: Functions injected into Sass context during Sprockets evaluation.
90
+ #
91
+ # This module may be extended to add global functionality to all Sprockets
92
+ # Sass environments. Though, scoping your functions to just your environment
93
+ # is preferred.
94
+ #
95
+ # module Sprockets::SasscProcessor::Functions
96
+ # def asset_path(path, options = {})
97
+ # end
98
+ # end
99
+ #
100
+ module Functions
101
+ # Public: Generate a url for asset path.
102
+ #
103
+ # Default implementation is deprecated. Currently defaults to
104
+ # Context#asset_path.
105
+ #
106
+ # Will raise NotImplementedError in the future. Users should provide their
107
+ # own base implementation.
108
+ #
109
+ # Returns a SassC::Script::Value::String.
110
+ def asset_path(path, options = {})
111
+ path = path.value
112
+
113
+ path, _, query, fragment = URI.split(path)[5..8]
114
+ path = sprockets_context.asset_path(path, options)
115
+ query = "?#{query}" if query
116
+ fragment = "##{fragment}" if fragment
117
+
118
+ Autoload::SassC::Script::Value::String.new("#{path}#{query}#{fragment}", :string)
119
+ end
120
+
121
+ # Public: Generate a asset url() link.
122
+ #
123
+ # path - SassC::Script::Value::String URL path
124
+ #
125
+ # Returns a SassC::Script::Value::String.
126
+ def asset_url(path, options = {})
127
+ Autoload::SassC::Script::Value::String.new("url(#{asset_path(path, options).value})")
128
+ end
129
+
130
+ # Public: Generate url for image path.
131
+ #
132
+ # path - SassC::Script::Value::String URL path
133
+ #
134
+ # Returns a SassC::Script::Value::String.
135
+ def image_path(path)
136
+ asset_path(path, type: :image)
137
+ end
138
+
139
+ # Public: Generate a image url() link.
140
+ #
141
+ # path - SassC::Script::Value::String URL path
142
+ #
143
+ # Returns a SassC::Script::Value::String.
144
+ def image_url(path)
145
+ asset_url(path, type: :image)
146
+ end
147
+
148
+ # Public: Generate url for video path.
149
+ #
150
+ # path - SassC::Script::Value::String URL path
151
+ #
152
+ # Returns a SassC::Script::Value::String.
153
+ def video_path(path)
154
+ asset_path(path, type: :video)
155
+ end
156
+
157
+ # Public: Generate a video url() link.
158
+ #
159
+ # path - SassC::Script::Value::String URL path
160
+ #
161
+ # Returns a SassC::Script::Value::String.
162
+ def video_url(path)
163
+ asset_url(path, type: :video)
164
+ end
165
+
166
+ # Public: Generate url for audio path.
167
+ #
168
+ # path - SassC::Script::Value::String URL path
169
+ #
170
+ # Returns a SassC::Script::Value::String.
171
+ def audio_path(path)
172
+ asset_path(path, type: :audio)
173
+ end
174
+
175
+ # Public: Generate a audio url() link.
176
+ #
177
+ # path - SassC::Script::Value::String URL path
178
+ #
179
+ # Returns a SassC::Script::Value::String.
180
+ def audio_url(path)
181
+ asset_url(path, type: :audio)
182
+ end
183
+
184
+ # Public: Generate url for font path.
185
+ #
186
+ # path - SassC::Script::Value::String URL path
187
+ #
188
+ # Returns a SassC::Script::Value::String.
189
+ def font_path(path)
190
+ asset_path(path, type: :font)
191
+ end
192
+
193
+ # Public: Generate a font url() link.
194
+ #
195
+ # path - SassC::Script::Value::String URL path
196
+ #
197
+ # Returns a SassC::Script::Value::String.
198
+ def font_url(path)
199
+ asset_url(path, type: :font)
200
+ end
201
+
202
+ # Public: Generate url for javascript path.
203
+ #
204
+ # path - SassC::Script::Value::String URL path
205
+ #
206
+ # Returns a SassC::Script::Value::String.
207
+ def javascript_path(path)
208
+ asset_path(path, type: :javascript)
209
+ end
210
+
211
+ # Public: Generate a javascript url() link.
212
+ #
213
+ # path - SassC::Script::Value::String URL path
214
+ #
215
+ # Returns a SassC::Script::Value::String.
216
+ def javascript_url(path)
217
+ asset_url(path, type: :javascript)
218
+ end
219
+
220
+ # Public: Generate url for stylesheet path.
221
+ #
222
+ # path - SassC::Script::Value::String URL path
223
+ #
224
+ # Returns a SassC::Script::Value::String.
225
+ def stylesheet_path(path)
226
+ asset_path(path, type: :stylesheet)
227
+ end
228
+
229
+ # Public: Generate a stylesheet url() link.
230
+ #
231
+ # path - SassC::Script::Value::String URL path
232
+ #
233
+ # Returns a SassC::Script::Value::String.
234
+ def stylesheet_url(path)
235
+ asset_url(path, type: :stylesheet)
236
+ end
237
+
238
+ # Public: Generate a data URI for asset path.
239
+ #
240
+ # path - SassC::Script::Value::String logical asset path
241
+ #
242
+ # Returns a SassC::Script::Value::String.
243
+ def asset_data_url(path)
244
+ url = sprockets_context.asset_data_uri(path.value)
245
+ Autoload::SassC::Script::Value::String.new("url(" + url + ")")
246
+ end
247
+
248
+ protected
249
+ # Public: The Environment.
250
+ #
251
+ # Returns Sprockets::Environment.
252
+ def sprockets_environment
253
+ options[:sprockets][:environment]
254
+ end
255
+
256
+ # Public: Mutatable set of dependencies.
257
+ #
258
+ # Returns a Set.
259
+ def sprockets_dependencies
260
+ options[:sprockets][:dependencies]
261
+ end
262
+
263
+ # Deprecated: Get the Context instance. Use APIs on
264
+ # sprockets_environment or sprockets_dependencies directly.
265
+ #
266
+ # Returns a Context instance.
267
+ def sprockets_context
268
+ options[:sprockets][:context]
269
+ end
270
+
271
+ end
272
+
273
+ def engine_options(input, context)
274
+ merge_options({
275
+ filename: input[:filename],
276
+ syntax: self.class.syntax,
277
+ load_paths: input[:environment].paths,
278
+ importer: @importer_class,
279
+ source_map_contents: false,
280
+ source_map_file: "#{input[:filename]}.map",
281
+ omit_source_map_url: true,
282
+ sprockets: {
283
+ context: context,
284
+ environment: input[:environment],
285
+ dependencies: context.metadata[:dependencies]
286
+ }
287
+ })
288
+ end
289
+ end
290
+
291
+
292
+ class ScsscProcessor < SasscProcessor
293
+ def self.syntax
294
+ :scss
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+ require 'time'
4
+ require 'rack/utils'
5
+
6
+ module Sprockets
7
+ # `Server` is a concern mixed into `Environment` and
8
+ # `CachedEnvironment` that provides a Rack compatible `call`
9
+ # interface and url generation helpers.
10
+ module Server
11
+ # Supported HTTP request methods.
12
+ ALLOWED_REQUEST_METHODS = ['GET', 'HEAD'].to_set.freeze
13
+
14
+ # `call` implements the Rack 1.x specification which accepts an
15
+ # `env` Hash and returns a three item tuple with the status code,
16
+ # headers, and body.
17
+ #
18
+ # Mapping your environment at a url prefix will serve all assets
19
+ # in the path.
20
+ #
21
+ # map "/assets" do
22
+ # run Sprockets::Environment.new
23
+ # end
24
+ #
25
+ # A request for `"/assets/foo/bar.js"` will search your
26
+ # environment for `"foo/bar.js"`.
27
+ def call(env)
28
+ start_time = Time.now.to_f
29
+ time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
30
+
31
+ unless ALLOWED_REQUEST_METHODS.include? env['REQUEST_METHOD']
32
+ return method_not_allowed_response
33
+ end
34
+
35
+ msg = "Served asset #{env['PATH_INFO']} -"
36
+
37
+ # Extract the path from everything after the leading slash
38
+ path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
39
+
40
+ unless path.valid_encoding?
41
+ return bad_request_response(env)
42
+ end
43
+
44
+ # Strip fingerprint
45
+ if fingerprint = path_fingerprint(path)
46
+ path = path.sub("-#{fingerprint}", '')
47
+ end
48
+
49
+ # URLs containing a `".."` are rejected for security reasons.
50
+ if forbidden_request?(path)
51
+ return forbidden_response(env)
52
+ end
53
+
54
+ if fingerprint
55
+ if_match = fingerprint
56
+ elsif env['HTTP_IF_MATCH']
57
+ if_match = env['HTTP_IF_MATCH'][/"(\w+)"$/, 1]
58
+ end
59
+
60
+ if env['HTTP_IF_NONE_MATCH']
61
+ if_none_match = env['HTTP_IF_NONE_MATCH'][/"(\w+)"$/, 1]
62
+ end
63
+
64
+ # Look up the asset.
65
+ asset = find_asset(path)
66
+
67
+ if asset.nil?
68
+ status = :not_found
69
+ elsif fingerprint && asset.etag != fingerprint
70
+ status = :not_found
71
+ elsif if_match && asset.etag != if_match
72
+ status = :precondition_failed
73
+ elsif if_none_match && asset.etag == if_none_match
74
+ status = :not_modified
75
+ else
76
+ status = :ok
77
+ end
78
+
79
+ case status
80
+ when :ok
81
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
82
+ ok_response(asset, env)
83
+ when :not_modified
84
+ logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
85
+ not_modified_response(env, if_none_match)
86
+ when :not_found
87
+ logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
88
+ not_found_response(env)
89
+ when :precondition_failed
90
+ logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
91
+ precondition_failed_response(env)
92
+ end
93
+ rescue Exception => e
94
+ logger.error "Error compiling asset #{path}:"
95
+ logger.error "#{e.class.name}: #{e.message}"
96
+
97
+ case File.extname(path)
98
+ when ".js"
99
+ # Re-throw JavaScript asset exceptions to the browser
100
+ logger.info "#{msg} 500 Internal Server Error\n\n"
101
+ return javascript_exception_response(e)
102
+ when ".css"
103
+ # Display CSS asset exceptions in the browser
104
+ logger.info "#{msg} 500 Internal Server Error\n\n"
105
+ return css_exception_response(e)
106
+ else
107
+ raise
108
+ end
109
+ end
110
+
111
+ private
112
+ def forbidden_request?(path)
113
+ # Prevent access to files elsewhere on the file system
114
+ #
115
+ # http://example.org/assets/../../../etc/passwd
116
+ #
117
+ path.include?("..") || absolute_path?(path) || path.include?("://")
118
+ end
119
+
120
+ def head_request?(env)
121
+ env['REQUEST_METHOD'] == 'HEAD'
122
+ end
123
+
124
+ # Returns a 200 OK response tuple
125
+ def ok_response(asset, env)
126
+ if head_request?(env)
127
+ [ 200, headers(env, asset, 0), [] ]
128
+ else
129
+ [ 200, headers(env, asset, asset.length), asset ]
130
+ end
131
+ end
132
+
133
+ # Returns a 304 Not Modified response tuple
134
+ def not_modified_response(env, etag)
135
+ [ 304, cache_headers(env, etag), [] ]
136
+ end
137
+
138
+ # Returns a 400 Forbidden response tuple
139
+ def bad_request_response(env)
140
+ if head_request?(env)
141
+ [ 400, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
142
+ else
143
+ [ 400, { "Content-Type" => "text/plain", "Content-Length" => "11" }, [ "Bad Request" ] ]
144
+ end
145
+ end
146
+
147
+ # Returns a 403 Forbidden response tuple
148
+ def forbidden_response(env)
149
+ if head_request?(env)
150
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "0" }, [] ]
151
+ else
152
+ [ 403, { "Content-Type" => "text/plain", "Content-Length" => "9" }, [ "Forbidden" ] ]
153
+ end
154
+ end
155
+
156
+ # Returns a 404 Not Found response tuple
157
+ def not_found_response(env)
158
+ if head_request?(env)
159
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
160
+ else
161
+ [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
162
+ end
163
+ end
164
+
165
+ def method_not_allowed_response
166
+ [ 405, { "Content-Type" => "text/plain", "Content-Length" => "18" }, [ "Method Not Allowed" ] ]
167
+ end
168
+
169
+ def precondition_failed_response(env)
170
+ if head_request?(env)
171
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "0", "X-Cascade" => "pass" }, [] ]
172
+ else
173
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
174
+ end
175
+ end
176
+
177
+ # Returns a JavaScript response that re-throws a Ruby exception
178
+ # in the browser
179
+ def javascript_exception_response(exception)
180
+ err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
181
+ body = "throw Error(#{err.inspect})"
182
+ [ 200, { "Content-Type" => "application/javascript", "Content-Length" => body.bytesize.to_s }, [ body ] ]
183
+ end
184
+
185
+ # Returns a CSS response that hides all elements on the page and
186
+ # displays the exception
187
+ def css_exception_response(exception)
188
+ message = "\n#{exception.class.name}: #{exception.message}"
189
+ backtrace = "\n #{exception.backtrace.first}"
190
+
191
+ body = <<-CSS
192
+ html {
193
+ padding: 18px 36px;
194
+ }
195
+
196
+ head {
197
+ display: block;
198
+ }
199
+
200
+ body {
201
+ margin: 0;
202
+ padding: 0;
203
+ }
204
+
205
+ body > * {
206
+ display: none !important;
207
+ }
208
+
209
+ head:after, body:before, body:after {
210
+ display: block !important;
211
+ }
212
+
213
+ head:after {
214
+ font-family: sans-serif;
215
+ font-size: large;
216
+ font-weight: bold;
217
+ content: "Error compiling CSS asset";
218
+ }
219
+
220
+ body:before, body:after {
221
+ font-family: monospace;
222
+ white-space: pre-wrap;
223
+ }
224
+
225
+ body:before {
226
+ font-weight: bold;
227
+ content: "#{escape_css_content(message)}";
228
+ }
229
+
230
+ body:after {
231
+ content: "#{escape_css_content(backtrace)}";
232
+ }
233
+ CSS
234
+
235
+ [ 200, { "Content-Type" => "text/css; charset=utf-8", "Content-Length" => body.bytesize.to_s }, [ body ] ]
236
+ end
237
+
238
+ # Escape special characters for use inside a CSS content("...") string
239
+ def escape_css_content(content)
240
+ content.
241
+ gsub('\\', '\\\\005c ').
242
+ gsub("\n", '\\\\000a ').
243
+ gsub('"', '\\\\0022 ').
244
+ gsub('/', '\\\\002f ')
245
+ end
246
+
247
+ def cache_headers(env, etag)
248
+ headers = {}
249
+
250
+ # Set caching headers
251
+ headers["Cache-Control"] = +"public"
252
+ headers["ETag"] = %("#{etag}")
253
+
254
+ # If the request url contains a fingerprint, set a long
255
+ # expires on the response
256
+ if path_fingerprint(env["PATH_INFO"])
257
+ headers["Cache-Control"] << ", max-age=31536000, immutable"
258
+
259
+ # Otherwise set `must-revalidate` since the asset could be modified.
260
+ else
261
+ headers["Cache-Control"] << ", must-revalidate"
262
+ headers["Vary"] = "Accept-Encoding"
263
+ end
264
+
265
+ headers
266
+ end
267
+
268
+ def headers(env, asset, length)
269
+ headers = {}
270
+
271
+ # Set content length header
272
+ headers["Content-Length"] = length.to_s
273
+
274
+ # Set content type header
275
+ if type = asset.content_type
276
+ # Set charset param for text/* mime types
277
+ if type.start_with?("text/") && asset.charset
278
+ type += "; charset=#{asset.charset}"
279
+ end
280
+ headers["Content-Type"] = type
281
+ end
282
+
283
+ headers.merge(cache_headers(env, asset.etag))
284
+ end
285
+
286
+ # Gets ETag fingerprint.
287
+ #
288
+ # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
289
+ # # => "0aa2105d29558f3eb790d411d7d8fb66"
290
+ #
291
+ def path_fingerprint(path)
292
+ path[/-([0-9a-f]{7,128})\.[^.]+\z/, 1]
293
+ end
294
+ end
295
+ end