sprockets 2.3.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of sprockets might be problematic. Click here for more details.

Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +2 -2
  3. data/README.md +332 -115
  4. data/bin/sprockets +8 -0
  5. data/lib/rake/sprocketstask.rb +25 -13
  6. data/lib/sprockets/asset.rb +143 -205
  7. data/lib/sprockets/autoload/closure.rb +7 -0
  8. data/lib/sprockets/autoload/coffee_script.rb +7 -0
  9. data/lib/sprockets/autoload/eco.rb +7 -0
  10. data/lib/sprockets/autoload/ejs.rb +7 -0
  11. data/lib/sprockets/autoload/sass.rb +7 -0
  12. data/lib/sprockets/autoload/uglifier.rb +7 -0
  13. data/lib/sprockets/autoload/yui.rb +7 -0
  14. data/lib/sprockets/autoload.rb +11 -0
  15. data/lib/sprockets/base.rb +49 -257
  16. data/lib/sprockets/bower.rb +58 -0
  17. data/lib/sprockets/bundle.rb +65 -0
  18. data/lib/sprockets/cache/file_store.rb +165 -14
  19. data/lib/sprockets/cache/memory_store.rb +66 -0
  20. data/lib/sprockets/cache/null_store.rb +46 -0
  21. data/lib/sprockets/cache.rb +234 -0
  22. data/lib/sprockets/cached_environment.rb +69 -0
  23. data/lib/sprockets/closure_compressor.rb +53 -0
  24. data/lib/sprockets/coffee_script_processor.rb +25 -0
  25. data/lib/sprockets/coffee_script_template.rb +6 -0
  26. data/lib/sprockets/compressing.rb +74 -0
  27. data/lib/sprockets/configuration.rb +83 -0
  28. data/lib/sprockets/context.rb +125 -131
  29. data/lib/sprockets/dependencies.rb +73 -0
  30. data/lib/sprockets/digest_utils.rb +156 -0
  31. data/lib/sprockets/directive_processor.rb +209 -211
  32. data/lib/sprockets/eco_processor.rb +32 -0
  33. data/lib/sprockets/eco_template.rb +3 -35
  34. data/lib/sprockets/ejs_processor.rb +31 -0
  35. data/lib/sprockets/ejs_template.rb +3 -34
  36. data/lib/sprockets/encoding_utils.rb +258 -0
  37. data/lib/sprockets/engines.rb +45 -38
  38. data/lib/sprockets/environment.rb +17 -67
  39. data/lib/sprockets/erb_processor.rb +30 -0
  40. data/lib/sprockets/erb_template.rb +6 -0
  41. data/lib/sprockets/errors.rb +6 -13
  42. data/lib/sprockets/file_reader.rb +15 -0
  43. data/lib/sprockets/http_utils.rb +115 -0
  44. data/lib/sprockets/jst_processor.rb +35 -19
  45. data/lib/sprockets/legacy.rb +314 -0
  46. data/lib/sprockets/legacy_proc_processor.rb +35 -0
  47. data/lib/sprockets/legacy_tilt_processor.rb +29 -0
  48. data/lib/sprockets/loader.rb +176 -0
  49. data/lib/sprockets/manifest.rb +179 -98
  50. data/lib/sprockets/manifest_utils.rb +45 -0
  51. data/lib/sprockets/mime.rb +114 -32
  52. data/lib/sprockets/path_dependency_utils.rb +85 -0
  53. data/lib/sprockets/path_digest_utils.rb +47 -0
  54. data/lib/sprockets/path_utils.rb +282 -0
  55. data/lib/sprockets/paths.rb +81 -0
  56. data/lib/sprockets/processing.rb +157 -189
  57. data/lib/sprockets/processor_utils.rb +103 -0
  58. data/lib/sprockets/resolve.rb +208 -0
  59. data/lib/sprockets/sass_cache_store.rb +19 -15
  60. data/lib/sprockets/sass_compressor.rb +59 -0
  61. data/lib/sprockets/sass_functions.rb +2 -0
  62. data/lib/sprockets/sass_importer.rb +2 -29
  63. data/lib/sprockets/sass_processor.rb +285 -0
  64. data/lib/sprockets/sass_template.rb +4 -44
  65. data/lib/sprockets/server.rb +109 -84
  66. data/lib/sprockets/transformers.rb +145 -0
  67. data/lib/sprockets/uglifier_compressor.rb +63 -0
  68. data/lib/sprockets/uri_utils.rb +190 -0
  69. data/lib/sprockets/utils.rb +193 -44
  70. data/lib/sprockets/version.rb +1 -1
  71. data/lib/sprockets/yui_compressor.rb +65 -0
  72. data/lib/sprockets.rb +144 -53
  73. metadata +248 -238
  74. data/lib/sprockets/asset_attributes.rb +0 -126
  75. data/lib/sprockets/bundled_asset.rb +0 -79
  76. data/lib/sprockets/caching.rb +0 -96
  77. data/lib/sprockets/charset_normalizer.rb +0 -41
  78. data/lib/sprockets/index.rb +0 -99
  79. data/lib/sprockets/processed_asset.rb +0 -152
  80. data/lib/sprockets/processor.rb +0 -32
  81. data/lib/sprockets/safety_colons.rb +0 -28
  82. data/lib/sprockets/scss_template.rb +0 -13
  83. data/lib/sprockets/static_asset.rb +0 -57
  84. data/lib/sprockets/trail.rb +0 -90
@@ -1,9 +1,9 @@
1
1
  require 'time'
2
- require 'uri'
2
+ require 'rack/utils'
3
3
 
4
4
  module Sprockets
5
5
  # `Server` is a concern mixed into `Environment` and
6
- # `Index` that provides a Rack compatible `call`
6
+ # `CachedEnvironment` that provides a Rack compatible `call`
7
7
  # interface and url generation helpers.
8
8
  module Server
9
9
  # `call` implements the Rack 1.x specification which accepts an
@@ -23,59 +23,84 @@ module Sprockets
23
23
  start_time = Time.now.to_f
24
24
  time_elapsed = lambda { ((Time.now.to_f - start_time) * 1000).to_i }
25
25
 
26
- msg = "Served asset #{env['PATH_INFO']} -"
26
+ if env['REQUEST_METHOD'] != 'GET'
27
+ return method_not_allowed_response
28
+ end
27
29
 
28
- # Mark session as "skipped" so no `Set-Cookie` header is set
29
- env['rack.session.options'] ||= {}
30
- env['rack.session.options'][:defer] = true
31
- env['rack.session.options'][:skip] = true
30
+ msg = "Served asset #{env['PATH_INFO']} -"
32
31
 
33
32
  # Extract the path from everything after the leading slash
34
- path = unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
35
-
36
- # URLs containing a `".."` are rejected for security reasons.
37
- if forbidden_request?(path)
38
- return forbidden_response
39
- end
33
+ path = Rack::Utils.unescape(env['PATH_INFO'].to_s.sub(/^\//, ''))
40
34
 
41
35
  # Strip fingerprint
42
36
  if fingerprint = path_fingerprint(path)
43
37
  path = path.sub("-#{fingerprint}", '')
44
38
  end
45
39
 
40
+ # URLs containing a `".."` are rejected for security reasons.
41
+ if forbidden_request?(path)
42
+ return forbidden_response
43
+ end
44
+
46
45
  # Look up the asset.
47
- asset = find_asset(path, :bundle => !body_only?(env))
46
+ options = {}
47
+ options[:pipeline] = :self if body_only?(env)
48
48
 
49
- # `find_asset` returns nil if the asset doesn't exist
50
- if asset.nil?
51
- logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
49
+ asset = find_asset(path, options)
52
50
 
53
- # Return a 404 Not Found
54
- not_found_response
51
+ # 2.x/3.x compatibility hack. Just ignore fingerprints on ?body=1 requests.
52
+ # 3.x/4.x prefers strong validation of fingerprint to body contents, but
53
+ # 2.x just ignored it.
54
+ if asset && parse_asset_uri(asset.uri)[1][:pipeline] == "self"
55
+ fingerprint = nil
56
+ end
55
57
 
56
- # Check request headers `HTTP_IF_NONE_MATCH` against the asset digest
57
- elsif etag_match?(asset, env)
58
- logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
58
+ if fingerprint
59
+ if_match = fingerprint
60
+ elsif env['HTTP_IF_MATCH']
61
+ if_match = env['HTTP_IF_MATCH'][/^"(\w+)"$/, 1]
62
+ end
59
63
 
60
- # Return a 304 Not Modified
61
- not_modified_response(asset, env)
64
+ if env['HTTP_IF_NONE_MATCH']
65
+ if_none_match = env['HTTP_IF_NONE_MATCH'][/^"(\w+)"$/, 1]
66
+ end
62
67
 
68
+ if asset.nil?
69
+ status = :not_found
70
+ elsif fingerprint && asset.etag != fingerprint
71
+ status = :not_found
72
+ elsif if_match && asset.etag != if_match
73
+ status = :precondition_failed
74
+ elsif if_none_match && asset.etag == if_none_match
75
+ status = :not_modified
63
76
  else
64
- logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
77
+ status = :ok
78
+ end
65
79
 
66
- # Return a 200 with the asset contents
80
+ case status
81
+ when :ok
82
+ logger.info "#{msg} 200 OK (#{time_elapsed.call}ms)"
67
83
  ok_response(asset, env)
84
+ when :not_modified
85
+ logger.info "#{msg} 304 Not Modified (#{time_elapsed.call}ms)"
86
+ not_modified_response(env, if_none_match)
87
+ when :not_found
88
+ logger.info "#{msg} 404 Not Found (#{time_elapsed.call}ms)"
89
+ not_found_response
90
+ when :precondition_failed
91
+ logger.info "#{msg} 412 Precondition Failed (#{time_elapsed.call}ms)"
92
+ precondition_failed_response
68
93
  end
69
94
  rescue Exception => e
70
95
  logger.error "Error compiling asset #{path}:"
71
96
  logger.error "#{e.class.name}: #{e.message}"
72
97
 
73
- case content_type_of(path)
74
- when "application/javascript"
98
+ case File.extname(path)
99
+ when ".js"
75
100
  # Re-throw JavaScript asset exceptions to the browser
76
101
  logger.info "#{msg} 500 Internal Server Error\n\n"
77
102
  return javascript_exception_response(e)
78
- when "text/css"
103
+ when ".css"
79
104
  # Display CSS asset exceptions in the browser
80
105
  logger.info "#{msg} 500 Internal Server Error\n\n"
81
106
  return css_exception_response(e)
@@ -90,7 +115,17 @@ module Sprockets
90
115
  #
91
116
  # http://example.org/assets/../../../etc/passwd
92
117
  #
93
- path.include?("..")
118
+ path.include?("..") || absolute_path?(path)
119
+ end
120
+
121
+ # Returns a 200 OK response tuple
122
+ def ok_response(asset, env)
123
+ [ 200, headers(env, asset, asset.length), asset ]
124
+ end
125
+
126
+ # Returns a 304 Not Modified response tuple
127
+ def not_modified_response(env, etag)
128
+ [ 304, cache_headers(env, etag), [] ]
94
129
  end
95
130
 
96
131
  # Returns a 403 Forbidden response tuple
@@ -103,12 +138,20 @@ module Sprockets
103
138
  [ 404, { "Content-Type" => "text/plain", "Content-Length" => "9", "X-Cascade" => "pass" }, [ "Not found" ] ]
104
139
  end
105
140
 
141
+ def method_not_allowed_response
142
+ [ 405, { "Content-Type" => "text/plain", "Content-Length" => "18" }, [ "Method Not Allowed" ] ]
143
+ end
144
+
145
+ def precondition_failed_response
146
+ [ 412, { "Content-Type" => "text/plain", "Content-Length" => "19", "X-Cascade" => "pass" }, [ "Precondition Failed" ] ]
147
+ end
148
+
106
149
  # Returns a JavaScript response that re-throws a Ruby exception
107
150
  # in the browser
108
151
  def javascript_exception_response(exception)
109
- err = "#{exception.class.name}: #{exception.message}"
152
+ err = "#{exception.class.name}: #{exception.message}\n (in #{exception.backtrace[0]})"
110
153
  body = "throw Error(#{err.inspect})"
111
- [ 200, { "Content-Type" => "application/javascript", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
154
+ [ 200, { "Content-Type" => "application/javascript", "Content-Length" => body.bytesize.to_s }, [ body ] ]
112
155
  end
113
156
 
114
157
  # Returns a CSS response that hides all elements on the page and
@@ -161,7 +204,7 @@ module Sprockets
161
204
  }
162
205
  CSS
163
206
 
164
- [ 200, { "Content-Type" => "text/css;charset=utf-8", "Content-Length" => Rack::Utils.bytesize(body).to_s }, [ body ] ]
207
+ [ 200, { "Content-Type" => "text/css; charset=utf-8", "Content-Length" => body.bytesize.to_s }, [ body ] ]
165
208
  end
166
209
 
167
210
  # Escape special characters for use inside a CSS content("...") string
@@ -173,75 +216,57 @@ module Sprockets
173
216
  gsub('/', '\\\\002f ')
174
217
  end
175
218
 
176
- # Compare the requests `HTTP_IF_NONE_MATCH` against the assets digest
177
- def etag_match?(asset, env)
178
- env["HTTP_IF_NONE_MATCH"] == etag(asset)
179
- end
180
-
181
219
  # Test if `?body=1` or `body=true` query param is set
182
220
  def body_only?(env)
183
221
  env["QUERY_STRING"].to_s =~ /body=(1|t)/
184
222
  end
185
223
 
186
- # Returns a 304 Not Modified response tuple
187
- def not_modified_response(asset, env)
188
- [ 304, {}, [] ]
189
- end
224
+ def cache_headers(env, etag)
225
+ headers = {}
190
226
 
191
- # Returns a 200 OK response tuple
192
- def ok_response(asset, env)
193
- [ 200, headers(env, asset, asset.length), asset ]
227
+ # Set caching headers
228
+ headers["Cache-Control"] = "public"
229
+ headers["ETag"] = %("#{etag}")
230
+
231
+ # If the request url contains a fingerprint, set a long
232
+ # expires on the response
233
+ if path_fingerprint(env["PATH_INFO"])
234
+ headers["Cache-Control"] << ", max-age=31536000"
235
+
236
+ # Otherwise set `must-revalidate` since the asset could be modified.
237
+ else
238
+ headers["Cache-Control"] << ", must-revalidate"
239
+ headers["Vary"] = "Accept-Encoding"
240
+ end
241
+
242
+ headers
194
243
  end
195
244
 
196
245
  def headers(env, asset, length)
197
- Hash.new.tap do |headers|
198
- # Set content type and length headers
199
- headers["Content-Type"] = asset.content_type
200
- headers["Content-Length"] = length.to_s
201
-
202
- # Set caching headers
203
- headers["Cache-Control"] = "public"
204
- headers["Last-Modified"] = asset.mtime.httpdate
205
- headers["ETag"] = etag(asset)
206
-
207
- # If the request url contains a fingerprint, set a long
208
- # expires on the response
209
- if path_fingerprint(env["PATH_INFO"])
210
- headers["Cache-Control"] << ", max-age=31536000"
211
-
212
- # Otherwise set `must-revalidate` since the asset could be modified.
213
- else
214
- headers["Cache-Control"] << ", must-revalidate"
246
+ headers = {}
247
+
248
+ # Set content length header
249
+ headers["Content-Length"] = length.to_s
250
+
251
+ # Set content type header
252
+ if type = asset.content_type
253
+ # Set charset param for text/* mime types
254
+ if type.start_with?("text/") && asset.charset
255
+ type += "; charset=#{asset.charset}"
215
256
  end
257
+ headers["Content-Type"] = type
216
258
  end
259
+
260
+ headers.merge(cache_headers(env, asset.etag))
217
261
  end
218
262
 
219
- # Gets digest fingerprint.
263
+ # Gets ETag fingerprint.
220
264
  #
221
265
  # "foo-0aa2105d29558f3eb790d411d7d8fb66.js"
222
266
  # # => "0aa2105d29558f3eb790d411d7d8fb66"
223
267
  #
224
268
  def path_fingerprint(path)
225
- path[/-([0-9a-f]{7,40})\.[^.]+$/, 1]
226
- end
227
-
228
- # URI.unescape is deprecated on 1.9. We need to use URI::Parser
229
- # if its available.
230
- if defined? URI::DEFAULT_PARSER
231
- def unescape(str)
232
- str = URI::DEFAULT_PARSER.unescape(str)
233
- str.force_encoding(Encoding.default_internal) if Encoding.default_internal
234
- str
235
- end
236
- else
237
- def unescape(str)
238
- URI.unescape(str)
239
- end
240
- end
241
-
242
- # Helper to quote the assets digest for use as an ETag.
243
- def etag(asset)
244
- %("#{asset.digest}")
269
+ path[/-([0-9a-f]{7,128})\.[^.]+\z/, 1]
245
270
  end
246
271
  end
247
272
  end
@@ -0,0 +1,145 @@
1
+ require 'sprockets/http_utils'
2
+ require 'sprockets/processor_utils'
3
+ require 'sprockets/utils'
4
+
5
+ module Sprockets
6
+ module Transformers
7
+ include HTTPUtils, ProcessorUtils, Utils
8
+
9
+ # Public: Two level mapping of a source mime type to a target mime type.
10
+ #
11
+ # environment.transformers
12
+ # # => { 'text/coffeescript' => {
13
+ # 'application/javascript' => ConvertCoffeeScriptToJavaScript
14
+ # }
15
+ # }
16
+ #
17
+ def transformers
18
+ config[:transformers]
19
+ end
20
+
21
+ # Public: Register a transformer from and to a mime type.
22
+ #
23
+ # from - String mime type
24
+ # to - String mime type
25
+ # proc - Callable block that accepts an input Hash.
26
+ #
27
+ # Examples
28
+ #
29
+ # register_transformer 'text/coffeescript', 'application/javascript',
30
+ # ConvertCoffeeScriptToJavaScript
31
+ #
32
+ # register_transformer 'image/svg+xml', 'image/png', ConvertSvgToPng
33
+ #
34
+ # Returns nothing.
35
+ def register_transformer(from, to, proc)
36
+ self.config = hash_reassoc(config, :registered_transformers, from) do |transformers|
37
+ transformers.merge(to => proc)
38
+ end
39
+ compute_transformers!
40
+ end
41
+
42
+ # Internal: Resolve target mime type that the source type should be
43
+ # transformed to.
44
+ #
45
+ # type - String from mime type
46
+ # accept - String accept type list (default: '*/*')
47
+ #
48
+ # Examples
49
+ #
50
+ # resolve_transform_type('text/plain', 'text/plain')
51
+ # # => 'text/plain'
52
+ #
53
+ # resolve_transform_type('image/svg+xml', 'image/png, image/*')
54
+ # # => 'image/png'
55
+ #
56
+ # resolve_transform_type('text/css', 'image/png')
57
+ # # => nil
58
+ #
59
+ # Returns String mime type or nil is no type satisfied the accept value.
60
+ def resolve_transform_type(type, accept)
61
+ find_best_mime_type_match(accept || '*/*', [type].compact + config[:transformers][type].keys)
62
+ end
63
+
64
+ # Internal: Expand accept type list to include possible transformed types.
65
+ #
66
+ # parsed_accepts - Array of accept q values
67
+ #
68
+ # Examples
69
+ #
70
+ # expand_transform_accepts([['application/javascript', 1.0]])
71
+ # # => [['application/javascript', 1.0], ['text/coffeescript', 0.8]]
72
+ #
73
+ # Returns an expanded Array of q values.
74
+ def expand_transform_accepts(parsed_accepts)
75
+ accepts = []
76
+ parsed_accepts.each do |(type, q)|
77
+ accepts.push([type, q])
78
+ config[:inverted_transformers][type].each do |subtype|
79
+ accepts.push([subtype, q * 0.8])
80
+ end
81
+ end
82
+ accepts
83
+ end
84
+
85
+ # Internal: Compose multiple transformer steps into a single processor
86
+ # function.
87
+ #
88
+ # transformers - Two level Hash of a source mime type to a target mime type
89
+ # types - Array of mime type steps
90
+ #
91
+ # Returns Processor.
92
+ def compose_transformers(transformers, types)
93
+ if types.length < 2
94
+ raise ArgumentError, "too few transform types: #{types.inspect}"
95
+ end
96
+
97
+ i = 0
98
+ processors = []
99
+
100
+ loop do
101
+ src = types[i]
102
+ dst = types[i+1]
103
+ break unless src && dst
104
+
105
+ unless processor = transformers[src][dst]
106
+ raise ArgumentError, "missing transformer for type: #{src} to #{dst}"
107
+ end
108
+ processors.concat config[:postprocessors][src]
109
+ processors << processor
110
+ processors.concat config[:preprocessors][dst]
111
+
112
+ i += 1
113
+ end
114
+
115
+ if processors.size > 1
116
+ compose_processors(*processors.reverse)
117
+ elsif processors.size == 1
118
+ processors.first
119
+ end
120
+ end
121
+
122
+ private
123
+ def compute_transformers!
124
+ registered_transformers = self.config[:registered_transformers]
125
+ transformers = Hash.new { {} }
126
+ inverted_transformers = Hash.new { Set.new }
127
+
128
+ registered_transformers.keys.flat_map do |key|
129
+ dfs_paths([key]) { |k| registered_transformers[k].keys }
130
+ end.each do |types|
131
+ src, dst = types.first, types.last
132
+ processor = compose_transformers(registered_transformers, types)
133
+
134
+ transformers[src] = {} unless transformers.key?(src)
135
+ transformers[src][dst] = processor
136
+
137
+ inverted_transformers[dst] = Set.new unless inverted_transformers.key?(dst)
138
+ inverted_transformers[dst] << src
139
+ end
140
+
141
+ self.config = hash_reassoc(config, :transformers) { transformers }
142
+ self.config = hash_reassoc(config, :inverted_transformers) { inverted_transformers }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,63 @@
1
+ require 'sprockets/autoload'
2
+
3
+ module Sprockets
4
+ # Public: Uglifier/Uglify compressor.
5
+ #
6
+ # To accept the default options
7
+ #
8
+ # environment.register_bundle_processor 'application/javascript',
9
+ # Sprockets::UglifierCompressor
10
+ #
11
+ # Or to pass options to the Uglifier class.
12
+ #
13
+ # environment.register_bundle_processor 'application/javascript',
14
+ # Sprockets::UglifierCompressor.new(comments: :copyright)
15
+ #
16
+ class UglifierCompressor
17
+ VERSION = '1'
18
+
19
+ # Public: Return singleton instance with default options.
20
+ #
21
+ # Returns UglifierCompressor 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 self.cache_key
31
+ instance.cache_key
32
+ end
33
+
34
+ attr_reader :cache_key
35
+
36
+ def initialize(options = {})
37
+ # Feature detect Uglifier 2.0 option support
38
+ if Autoload::Uglifier::DEFAULTS[:copyright]
39
+ # Uglifier < 2.x
40
+ options[:copyright] ||= false
41
+ else
42
+ # Uglifier >= 2.x
43
+ options[:copyright] ||= :none
44
+ end
45
+
46
+ @uglifier = Autoload::Uglifier.new(options)
47
+
48
+ @cache_key = [
49
+ self.class.name,
50
+ Autoload::Uglifier::VERSION,
51
+ VERSION,
52
+ options
53
+ ].freeze
54
+ end
55
+
56
+ def call(input)
57
+ data = input[:data]
58
+ input[:cache].fetch(@cache_key + [data]) do
59
+ @uglifier.compile(data)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,190 @@
1
+ require 'uri'
2
+
3
+ module Sprockets
4
+ # Internal: Asset URI related parsing utilities. Mixed into Environment.
5
+ #
6
+ # An Asset URI identifies the compiled Asset result. It shares the file:
7
+ # scheme and requires an absolute path.
8
+ #
9
+ # Other query parameters
10
+ #
11
+ # type - String output content type. Otherwise assumed from file extension.
12
+ # This maybe different than the extension if the asset is transformed
13
+ # from one content type to another. For an example .coffee -> .js.
14
+ #
15
+ # id - Unique fingerprint of the entire asset and all its metadata. Assets
16
+ # will only have the same id if they serialize to an identical value.
17
+ #
18
+ # pipeline - String name of pipeline.
19
+ #
20
+ # encoding - A content encoding such as "gzip" or "deflate". NOT a charset
21
+ # like "utf-8".
22
+ #
23
+ module URIUtils
24
+ extend self
25
+
26
+ # Internal: Parse URI into component parts.
27
+ #
28
+ # uri - String uri
29
+ #
30
+ # Returns Array of components.
31
+ def split_uri(uri)
32
+ URI.split(uri)
33
+ end
34
+
35
+ # Internal: Join URI component parts into String.
36
+ #
37
+ # Returns String.
38
+ def join_uri(scheme, userinfo, host, port, registry, path, opaque, query, fragment)
39
+ URI::Generic.new(scheme, userinfo, host, port, registry, path, opaque, query, fragment).to_s
40
+ end
41
+
42
+ # Internal: Parse file: URI into component parts.
43
+ #
44
+ # uri - String uri
45
+ #
46
+ # Returns [scheme, host, path, query].
47
+ def split_file_uri(uri)
48
+ scheme, _, host, _, _, path, _, query, _ = URI.split(uri)
49
+
50
+ path = URI::Generic::DEFAULT_PARSER.unescape(path)
51
+ path.force_encoding(Encoding::UTF_8)
52
+
53
+ # Hack for parsing Windows "file:///C:/Users/IEUser" paths
54
+ path = path.gsub(/^\/([a-zA-Z]:)/, '\1')
55
+
56
+ [scheme, host, path, query]
57
+ end
58
+
59
+ # Internal: Join file: URI component parts into String.
60
+ #
61
+ # Returns String.
62
+ def join_file_uri(scheme, host, path, query)
63
+ str = "#{scheme}://"
64
+ str << host if host
65
+ path = "/#{path}" unless path.start_with?("/")
66
+ str << URI::Generic::DEFAULT_PARSER.escape(path)
67
+ str << "?#{query}" if query
68
+ str
69
+ end
70
+
71
+ # Internal: Check if String is a valid Asset URI.
72
+ #
73
+ # str - Possible String asset URI.
74
+ #
75
+ # Returns true or false.
76
+ def valid_asset_uri?(str)
77
+ # Quick prefix check before attempting a full parse
78
+ str.start_with?("file://") && parse_asset_uri(str) ? true : false
79
+ rescue URI::InvalidURIError
80
+ false
81
+ end
82
+
83
+ # Internal: Parse Asset URI.
84
+ #
85
+ # Examples
86
+ #
87
+ # parse("file:///tmp/js/application.coffee?type=application/javascript")
88
+ # # => "/tmp/js/application.coffee", {type: "application/javascript"}
89
+ #
90
+ # uri - String asset URI
91
+ #
92
+ # Returns String path and Hash of symbolized parameters.
93
+ def parse_asset_uri(uri)
94
+ scheme, _, path, query = split_file_uri(uri)
95
+
96
+ unless scheme == 'file'
97
+ raise URI::InvalidURIError, "expected file:// scheme: #{uri}"
98
+ end
99
+
100
+ return path, parse_uri_query_params(query)
101
+ end
102
+
103
+ # Internal: Build Asset URI.
104
+ #
105
+ # Examples
106
+ #
107
+ # build("/tmp/js/application.coffee", type: "application/javascript")
108
+ # # => "file:///tmp/js/application.coffee?type=application/javascript"
109
+ #
110
+ # path - String file path
111
+ # params - Hash of optional parameters
112
+ #
113
+ # Returns String URI.
114
+ def build_asset_uri(path, params = {})
115
+ join_file_uri("file", nil, path, encode_uri_query_params(params))
116
+ end
117
+
118
+ # Internal: Parse file-digest dependency URI.
119
+ #
120
+ # Examples
121
+ #
122
+ # parse("file-digest:/tmp/js/application.js")
123
+ # # => "/tmp/js/application.js"
124
+ #
125
+ # uri - String file-digest URI
126
+ #
127
+ # Returns String path.
128
+ def parse_file_digest_uri(uri)
129
+ scheme, _, path, _ = split_file_uri(uri)
130
+
131
+ unless scheme == 'file-digest'
132
+ raise URI::InvalidURIError, "expected file-digest scheme: #{uri}"
133
+ end
134
+
135
+ path
136
+ end
137
+
138
+ # Internal: Build file-digest dependency URI.
139
+ #
140
+ # Examples
141
+ #
142
+ # build("/tmp/js/application.js")
143
+ # # => "file-digest:/tmp/js/application.js"
144
+ #
145
+ # path - String file path
146
+ #
147
+ # Returns String URI.
148
+ def build_file_digest_uri(path)
149
+ join_file_uri("file-digest", nil, path, nil)
150
+ end
151
+
152
+ # Internal: Serialize hash of params into query string.
153
+ #
154
+ # params - Hash of params to serialize
155
+ #
156
+ # Returns String query or nil if empty.
157
+ def encode_uri_query_params(params)
158
+ query = []
159
+
160
+ params.each do |key, value|
161
+ case value
162
+ when Integer
163
+ query << "#{key}=#{value}"
164
+ when String, Symbol
165
+ query << "#{key}=#{URI::Generic::DEFAULT_PARSER.escape(value.to_s)}"
166
+ when TrueClass
167
+ query << "#{key}"
168
+ when FalseClass, NilClass
169
+ else
170
+ raise TypeError, "unexpected type: #{value.class}"
171
+ end
172
+ end
173
+
174
+ "#{query.join('&')}" if query.any?
175
+ end
176
+
177
+ # Internal: Parse query string into hash of params
178
+ #
179
+ # query - String query string
180
+ #
181
+ # Return Hash of params.
182
+ def parse_uri_query_params(query)
183
+ query.to_s.split('&').reduce({}) do |h, p|
184
+ k, v = p.split('=', 2)
185
+ v = URI::Generic::DEFAULT_PARSER.unescape(v) if v
186
+ h.merge(k.to_sym => v || true)
187
+ end
188
+ end
189
+ end
190
+ end